Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.89% covered (warning)
57.89%
44 / 76
60.00% covered (warning)
60.00%
21 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
App
57.89% covered (warning)
57.89%
44 / 76
60.00% covered (warning)
60.00%
21 / 35
173.68
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addErrorHandler
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 createdWith
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 boot
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 run
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 serve
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 config
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 core
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 bootstrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 plugin
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 section
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 collection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 icons
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 navigation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 meta
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 role
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 allow
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 user
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 node
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 load
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 group
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 staticRoute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 before
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 after
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMiddleware
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 middleware
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 logger
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 container
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 factory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 router
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __call
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray;
6
7use BadMethodCallException;
8use Celema\Container\Container;
9use Celema\Container\Entry;
10use Celema\Core\App as CoreApp;
11use Celema\Core\Factory\Factory;
12use Celema\Core\Factory\Nyholm;
13use Celema\Core\Plugin as CorePlugin;
14use Celema\Router\AddsRoutes;
15use Celema\Router\After;
16use Celema\Router\Before;
17use Celema\Router\Route;
18use Celema\Router\RouteAdder;
19use Celema\Router\Router;
20use Closure;
21use Cosray\Icons\Provider as IconProvider;
22use Cosray\Node\Types;
23use Cosray\Panel\Dashboard;
24use Cosray\View\Boiler\Error\Handler as ErrorHandler;
25use Override;
26use Psr\Http\Message\ResponseInterface as Response;
27use Psr\Http\Message\ServerRequestInterface as Request;
28use Psr\Http\Server\MiddlewareInterface as Middleware;
29use Psr\Log\LoggerInterface as Logger;
30
31/**
32 * Convenience facade for CMS applications.
33 */
34class App implements RouteAdder
35{
36    use AddsRoutes;
37
38    protected readonly CoreApp $core;
39    protected readonly Bootstrap $bootstrap;
40    protected bool $booted = false;
41
42    /** @var ?Closure(Config): Bootstrap */
43    private static ?Closure $createBootstrap = null;
44
45    public function __construct(
46        public readonly Config $config,
47        Factory $factory,
48        Router $router,
49        Container $container,
50    ) {
51        $this->core = new CoreApp($factory, $router, $container);
52        $this->bootstrap = self::$createBootstrap !== null ? (self::$createBootstrap)($config) : new Bootstrap($config);
53        $container->add(self::class, $this);
54        $this->addErrorHandler($container, $factory);
55    }
56
57    protected function addErrorHandler(Container $container, Factory $factory): void
58    {
59        if ($this->config->error->enabled) {
60            $this->core->errorHandler(
61                new ErrorHandler($this->config, $factory, new ContainerLogger($container))->create(),
62            );
63        }
64    }
65
66    /**
67     * Apps created while $create runs get their bootstrap from $bootstrap,
68     * as the test kit builds a site's app from its unchanged boot file.
69     *
70     * @internal
71     *
72     * @template T
73     *
74     * @param Closure(Config): Bootstrap $bootstrap
75     * @param Closure(): T $create
76     * @return T
77     */
78    public static function createdWith(Closure $bootstrap, Closure $create): mixed
79    {
80        $previous = self::$createBootstrap;
81        self::$createBootstrap = $bootstrap;
82
83        try {
84            return $create();
85        } finally {
86            self::$createBootstrap = $previous;
87        }
88    }
89
90    /** @param array<string, mixed> $settings */
91    public static function create(string $root, array $settings = []): self
92    {
93        $config = new Config($root, $settings);
94
95        return new self(
96            $config,
97            new Nyholm(),
98            new Router($config->app->urlPrefix),
99            new Container(),
100        );
101    }
102
103    public function boot(): self
104    {
105        if (!$this->booted) {
106            $this->core->load($this->bootstrap);
107            $this->core->addRoute($this->bootstrap->catchallRoute());
108            $this->booted = true;
109        }
110
111        return $this;
112    }
113
114    public function run(?Request $request = null): Response|false
115    {
116        $this->boot();
117
118        return $this->core->run($request);
119    }
120
121    /**
122     * The front controller's entry point: handles requests until the worker
123     * retires when started as a FrankenPHP worker, otherwise the current
124     * request. See Celema\Core\App::serve() for the limits.
125     */
126    public function serve(?int $maxRequests = null, ?int $maxMemory = null): Response|false
127    {
128        $this->boot();
129
130        return $this->core->serve($maxRequests, $maxMemory);
131    }
132
133    public function config(): Config
134    {
135        return $this->config;
136    }
137
138    public function core(): CoreApp
139    {
140        return $this->core;
141    }
142
143    public function bootstrap(): Bootstrap
144    {
145        return $this->bootstrap;
146    }
147
148    /** @param class-string<Plugin\Plugin>|Plugin\Plugin $plugin */
149    public function plugin(string|Plugin\Plugin $plugin): self
150    {
151        $this->bootstrap->plugin($plugin);
152
153        return $this;
154    }
155
156    public function section(string $name): Section
157    {
158        return $this->bootstrap->section($name);
159    }
160
161    /** @param class-string $class */
162    public function collection(string $class): Collection\Ref
163    {
164        return $this->bootstrap->collection($class);
165    }
166
167    /** @param class-string<IconProvider>|IconProvider $icons */
168    public function icons(string|IconProvider $icons, bool $replace = false): self
169    {
170        $this->bootstrap->icons($icons, $replace);
171
172        return $this;
173    }
174
175    public Dashboard $dashboard {
176        get => $this->bootstrap->dashboard;
177    }
178
179    public function navigation(): Navigation
180    {
181        return $this->bootstrap->navigation();
182    }
183
184    public function meta(): Types
185    {
186        return $this->bootstrap->meta();
187    }
188
189    public function role(string $name, string $label): self
190    {
191        $this->bootstrap->policy()->role($name, $label);
192
193        return $this;
194    }
195
196    public function allow(string $principal, string ...$permissions): self
197    {
198        $this->bootstrap->policy()->allow($principal, ...$permissions);
199
200        return $this;
201    }
202
203    /** @param class-string<User> $class */
204    public function user(string $class): self
205    {
206        $this->bootstrap->user($class);
207
208        return $this;
209    }
210
211    /** @param class-string $class */
212    public function node(string $class): self
213    {
214        $this->bootstrap->node($class);
215
216        return $this;
217    }
218
219    /**
220     * @param non-empty-string $id
221     * @param class-string<Renderer> $class
222     */
223    public function renderer(string $id, string $class): Entry
224    {
225        return $this->bootstrap->renderer($id, $class);
226    }
227
228    public function load(CorePlugin $plugin): self
229    {
230        $this->core->load($plugin);
231
232        return $this;
233    }
234
235    #[Override]
236    public function addRoute(Route $route): Route
237    {
238        return $this->core->addRoute($route);
239    }
240
241    #[Override]
242    public function group(
243        string $patternPrefix,
244        Closure $createClosure,
245        string $namePrefix = '',
246    ): void {
247        $this->core->group($patternPrefix, $createClosure, $namePrefix);
248    }
249
250    public function staticRoute(string $prefix, string $path, string $name = ''): self
251    {
252        $this->core->staticRoute($prefix, $path, $name);
253
254        return $this;
255    }
256
257    public function before(Before $beforeHandler): self
258    {
259        $this->core->before($beforeHandler);
260
261        return $this;
262    }
263
264    public function after(After $afterHandler): self
265    {
266        $this->core->after($afterHandler);
267
268        return $this;
269    }
270
271    /** @return list<Middleware> */
272    public function getMiddleware(): array
273    {
274        return $this->core->getMiddleware();
275    }
276
277    public function middleware(Middleware ...$middleware): self
278    {
279        $this->core->middleware(...$middleware);
280
281        return $this;
282    }
283
284    public function logger(Logger|callable $logger): self
285    {
286        $this->core->logger($logger);
287
288        return $this;
289    }
290
291    public function container(): Container
292    {
293        return $this->core->container();
294    }
295
296    public function factory(): Factory
297    {
298        return $this->core->factory();
299    }
300
301    public function router(): Router
302    {
303        return $this->core->router();
304    }
305
306    /**
307     * @param non-empty-string $key
308     * @param class-string|object $value
309     */
310    public function register(string $key, object|string $value): Entry
311    {
312        return $this->core->register($key, $value);
313    }
314
315    /** @param list<mixed> $args */
316    public function __call(string $method, array $args): mixed
317    {
318        $coreCallable = is_callable([$this->core, $method]);
319        $bootstrapCallable = is_callable([$this->bootstrap, $method]);
320
321        if ($coreCallable && $bootstrapCallable) {
322            throw new BadMethodCallException("Ambiguous CMS app method: {$method}");
323        }
324
325        if ($bootstrapCallable) {
326            return $this->bootstrap->{$method}(...$args);
327        }
328
329        if ($coreCallable) {
330            return $this->core->{$method}(...$args);
331        }
332
333        throw new BadMethodCallException("Unknown CMS app method: {$method}");
334    }
335}