Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
215 / 215
100.00% covered (success)
100.00%
26 / 26
CRAP
100.00% covered (success)
100.00%
1 / 1
RouteCollection
100.00% covered (success)
100.00%
215 / 215
100.00% covered (success)
100.00%
26 / 26
68
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __call
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 __get
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOrigin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRouteName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addRoute
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 notFound
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRouteNotFound
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 add
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 makeRoute
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addSimple
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeRouteActionFromArray
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 post
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 put
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 patch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 options
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 redirect
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 group
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 namespace
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 resource
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
8
 presenter
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
1 / 1
10
 count
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 jsonSerialize
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Routing Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Routing;
11
12use BadMethodCallException;
13use Closure;
14use Error;
15use Framework\HTTP\Method;
16use InvalidArgumentException;
17use LogicException;
18
19/**
20 * Class RouteCollection.
21 *
22 * @property-read string|null $name
23 * @property-read string $origin
24 * @property-read Router $router
25 * @property-read array<string, Route[]> $routes
26 *
27 * @package routing
28 */
29class RouteCollection implements \Countable, \JsonSerializable
30{
31    protected Router $router;
32    protected string $origin;
33    protected ?string $name;
34    /**
35     * Array of HTTP Methods as keys and array of Routes as values.
36     *
37     * @var array<string, Route[]>
38     */
39    protected array $routes = [];
40    /**
41     * The Error 404 page action.
42     */
43    protected Closure | string $notFoundAction;
44
45    /**
46     * RouteCollection constructor.
47     *
48     * @param Router $router A Router instance
49     * @param string $origin URL Origin. A string in the following format:
50     * `{scheme}://{hostname}[:{port}]`
51     * @param string|null $name The collection name
52     */
53    public function __construct(Router $router, string $origin, string $name = null)
54    {
55        $this->router = $router;
56        $this->setOrigin($origin);
57        $this->name = $name;
58    }
59
60    /**
61     * @param string $method
62     * @param array<int,mixed> $arguments
63     *
64     * @throws BadMethodCallException for method not allowed or method not found
65     *
66     * @return Route|null
67     */
68    public function __call(string $method, array $arguments)
69    {
70        if ($method === 'getRouteNotFound') {
71            return $this->getRouteNotFound();
72        }
73        $class = static::class;
74        if (\method_exists($this, $method)) {
75            throw new BadMethodCallException(
76                "Method not allowed: {$class}::{$method}"
77            );
78        }
79        throw new BadMethodCallException("Method not found: {$class}::{$method}");
80    }
81
82    /**
83     * @param string $property
84     *
85     * @throws Error if cannot access property
86     *
87     * @return mixed
88     */
89    public function __get(string $property) : mixed
90    {
91        if ($property === 'name') {
92            return $this->name;
93        }
94        if ($property === 'notFoundAction') {
95            return $this->notFoundAction;
96        }
97        if ($property === 'origin') {
98            return $this->origin;
99        }
100        if ($property === 'router') {
101            return $this->router;
102        }
103        if ($property === 'routes') {
104            return $this->routes;
105        }
106        throw new Error(
107            'Cannot access property ' . static::class . '::$' . $property
108        );
109    }
110
111    public function __isset(string $property) : bool
112    {
113        return isset($this->{$property});
114    }
115
116    /**
117     * @param string $origin
118     *
119     * @return static
120     */
121    protected function setOrigin(string $origin) : static
122    {
123        $this->origin = \ltrim($origin, '/');
124        return $this;
125    }
126
127    /**
128     * Get a Route name.
129     *
130     * @param string $name The current Route name
131     *
132     * @return string The Route name prefixed with the collection name and a
133     * dot if it is set
134     */
135    protected function getRouteName(string $name) : string
136    {
137        if (isset($this->name)) {
138            $name = $this->name . '.' . $name;
139        }
140        return $name;
141    }
142
143    /**
144     * @param string $httpMethod
145     * @param Route $route
146     *
147     * @throws InvalidArgumentException for invalid method
148     *
149     * @return static
150     */
151    protected function addRoute(string $httpMethod, Route $route) : static
152    {
153        $method = \strtoupper($httpMethod);
154        if ( ! \in_array($method, [
155            'DELETE',
156            'GET',
157            'OPTIONS',
158            'PATCH',
159            'POST',
160            'PUT',
161        ], true)) {
162            throw new InvalidArgumentException('Invalid method: ' . $httpMethod);
163        }
164        $this->routes[$method][] = $route;
165        return $this;
166    }
167
168    /**
169     * Sets the Route Not Found action for this collection.
170     *
171     * @param Closure|string $action the Route function to run when no Route
172     * path is found for this collection
173     */
174    public function notFound(Closure | string $action) : void
175    {
176        $this->notFoundAction = $action;
177    }
178
179    /**
180     * Gets the Route Not Found for this collection.
181     *
182     * @see RouteCollection::notFound()
183     *
184     * @return Route|null The Route containing the Not Found Action or null if
185     * the Action was not set
186     */
187    protected function getRouteNotFound() : ?Route
188    {
189        if (isset($this->notFoundAction)) {
190            $this->router->getResponse()->setStatus(404);
191            return (new Route(
192                $this->router,
193                $this->router->getMatchedOrigin(),
194                $this->router->getMatchedPath(),
195                $this->notFoundAction
196            ))->setName(
197                $this->getRouteName('collection-not-found')
198            );
199        }
200        return null;
201    }
202
203    /**
204     * Adds a Route to match many HTTP Methods.
205     *
206     * @param array<int,string> $httpMethods The HTTP Methods
207     * @param string $path The URL path
208     * @param array<int,string>|Closure|string $action The Route action
209     * @param string|null $name The Route name
210     *
211     * @see Method::DELETE
212     * @see Method::GET
213     * @see Method::OPTIONS
214     * @see Method::PATCH
215     * @see Method::POST
216     * @see Method::PUT
217     *
218     * @return Route
219     */
220    public function add(
221        array $httpMethods,
222        string $path,
223        array | Closure | string $action,
224        string $name = null
225    ) : Route {
226        $route = $this->makeRoute($path, $action, $name);
227        foreach ($httpMethods as $method) {
228            $this->addRoute($method, $route);
229        }
230        return $route;
231    }
232
233    /**
234     * @param string $path
235     * @param array<int,string>|Closure|string $action
236     * @param string|null $name
237     *
238     * @return Route
239     */
240    protected function makeRoute(
241        string $path,
242        array | Closure | string $action,
243        string $name = null
244    ) : Route {
245        if (\is_array($action)) {
246            $action = $this->makeRouteActionFromArray($action);
247        }
248        $route = new Route($this->router, $this->origin, $path, $action);
249        if ($name !== null) {
250            $route->setName($this->getRouteName($name));
251        }
252        return $route;
253    }
254
255    /**
256     * @param string $method
257     * @param string $path
258     * @param array<int,string>|Closure|string $action
259     * @param string|null $name
260     *
261     * @return Route
262     */
263    protected function addSimple(
264        string $method,
265        string $path,
266        array | Closure | string $action,
267        string $name = null
268    ) : Route {
269        return $this->routes[$method][] = $this->makeRoute($path, $action, $name);
270    }
271
272    /**
273     * @param array<int,string> $action
274     *
275     * @return string
276     */
277    protected function makeRouteActionFromArray(array $action) : string
278    {
279        if (empty($action[0])) {
280            throw new LogicException(
281                'When adding a route action as array, the index 0 must be a FQCN'
282            );
283        }
284        if ( ! isset($action[1])) {
285            $action[1] = $this->router->getDefaultRouteActionMethod();
286        }
287        if ( ! isset($action[2])) {
288            $action[2] = '*';
289        }
290        if ($action[2] !== '') {
291            $action[2] = '/' . $action[2];
292        }
293        return $action[0] . '::' . $action[1] . $action[2];
294    }
295
296    /**
297     * Adds a Route to match the HTTP GET Method.
298     *
299     * @param string $path The URL path
300     * @param array<int,string>|Closure|string $action The Route action
301     * @param string|null $name The Route name
302     *
303     * @see Method::GET
304     *
305     * @return Route The Route added to the collection
306     */
307    public function get(
308        string $path,
309        array | Closure | string $action,
310        string $name = null
311    ) : Route {
312        return $this->addSimple('GET', $path, $action, $name);
313    }
314
315    /**
316     * Adds a Route to match the HTTP POST Method.
317     *
318     * @param string $path The URL path
319     * @param array<int,string>|Closure|string $action The Route action
320     * @param string|null $name The Route name
321     *
322     * @see Method::POST
323     *
324     * @return Route The Route added to the collection
325     */
326    public function post(
327        string $path,
328        array | Closure | string $action,
329        string $name = null
330    ) : Route {
331        return $this->addSimple('POST', $path, $action, $name);
332    }
333
334    /**
335     * Adds a Route to match the HTTP PUT Method.
336     *
337     * @param string $path The URL path
338     * @param array<int,string>|Closure|string $action The Route action
339     * @param string|null $name The Route name
340     *
341     * @see Method::PUT
342     *
343     * @return Route The Route added to the collection
344     */
345    public function put(
346        string $path,
347        array | Closure | string $action,
348        string $name = null
349    ) : Route {
350        return $this->addSimple('PUT', $path, $action, $name);
351    }
352
353    /**
354     * Adds a Route to match the HTTP PATCH Method.
355     *
356     * @param string $path The URL path
357     * @param array<int,string>|Closure|string $action The Route action
358     * @param string|null $name The Route name
359     *
360     * @see Method::PATCH
361     *
362     * @return Route The Route added to the collection
363     */
364    public function patch(
365        string $path,
366        array | Closure | string $action,
367        string $name = null
368    ) : Route {
369        return $this->addSimple('PATCH', $path, $action, $name);
370    }
371
372    /**
373     * Adds a Route to match the HTTP DELETE Method.
374     *
375     * @param string $path The URL path
376     * @param array<int,string>|Closure|string $action The Route action
377     * @param string|null $name The Route name
378     *
379     * @see Method::DELETE
380     *
381     * @return Route The Route added to the collection
382     */
383    public function delete(
384        string $path,
385        array | Closure | string $action,
386        string $name = null
387    ) : Route {
388        return $this->addSimple('DELETE', $path, $action, $name);
389    }
390
391    /**
392     * Adds a Route to match the HTTP OPTIONS Method.
393     *
394     * @param string $path The URL path
395     * @param array<int,string>|Closure|string $action The Route action
396     * @param string|null $name The Route name
397     *
398     * @see Method::OPTIONS
399     *
400     * @return Route The Route added to the collection
401     */
402    public function options(
403        string $path,
404        array | Closure | string $action,
405        string $name = null
406    ) : Route {
407        return $this->addSimple('OPTIONS', $path, $action, $name);
408    }
409
410    /**
411     * Adds a GET Route to match a path and automatically redirects to a URL.
412     *
413     * @param string $path The URL path
414     * @param string $location The URL to redirect
415     * @param int|null $code The status code of the response
416     *
417     * @return Route The Route added to the collection
418     */
419    public function redirect(string $path, string $location, int $code = null) : Route
420    {
421        $response = $this->router->getResponse();
422        return $this->addSimple(
423            'GET',
424            $path,
425            static function () use ($response, $location, $code) : void {
426                $response->redirect($location, [], $code);
427            }
428        );
429    }
430
431    /**
432     * Groups many Routes into a URL path.
433     *
434     * @param string $basePath The URL path to group in
435     * @param array<array<mixed|Route>|Route> $routes The Routes to be grouped
436     * @param array<string,mixed> $options Custom options passed to the Routes
437     *
438     * @return array<array<mixed|Route>|Route> The same $routes with updated paths and options
439     */
440    public function group(string $basePath, array $routes, array $options = []) : array
441    {
442        $basePath = \rtrim($basePath, '/');
443        foreach ($routes as $route) {
444            if (\is_array($route)) {
445                $this->group($basePath, $route, $options);
446                continue;
447            }
448            $route->setPath($basePath . $route->getPath());
449            if ($options) {
450                $specificOptions = $options;
451                if ($route->getOptions()) {
452                    $specificOptions = \array_replace_recursive($options, $route->getOptions());
453                }
454                $route->setOptions($specificOptions);
455            }
456        }
457        return $routes;
458    }
459
460    /**
461     * Updates Routes actions, which are strings, prepending a namespace.
462     *
463     * @param string $namespace The namespace
464     * @param array<array<mixed|Route>|Route> $routes The Routes
465     *
466     * @return array<array<mixed|Route>|Route> The same $routes with updated actions
467     */
468    public function namespace(string $namespace, array $routes) : array
469    {
470        $namespace = \trim($namespace, '\\');
471        foreach ($routes as $route) {
472            if (\is_array($route)) {
473                $this->namespace($namespace, $route);
474                continue;
475            }
476            if (\is_string($route->getAction())) {
477                $route->setAction($namespace . '\\' . $route->getAction());
478            }
479        }
480        return $routes;
481    }
482
483    /**
484     * Adds many Routes that can be used as a REST Resource.
485     *
486     * @param string $path The URL path
487     * @param string $class The name of the class where the resource will point
488     * @param string $baseName The base name used as a Route name prefix
489     * @param array<int,string> $except Actions not added. Allowed values are:
490     * index, create, show, update, replace and delete
491     * @param string $placeholder The placeholder. Normally it matches an id, a number
492     *
493     * @see ResourceInterface
494     * @see Router::$placeholders
495     *
496     * @return array<int,Route> The Routes added to the collection
497     */
498    public function resource(
499        string $path,
500        string $class,
501        string $baseName,
502        array $except = [],
503        string $placeholder = '{int}'
504    ) : array {
505        $path = \rtrim($path, '/') . '/';
506        $class .= '::';
507        if ($except) {
508            $except = \array_flip($except);
509        }
510        $routes = [];
511        if ( ! isset($except['index'])) {
512            $routes[] = $this->get(
513                $path,
514                $class . 'index/*',
515                $baseName . '.index'
516            );
517        }
518        if ( ! isset($except['create'])) {
519            $routes[] = $this->post(
520                $path,
521                $class . 'create/*',
522                $baseName . '.create'
523            );
524        }
525        if ( ! isset($except['show'])) {
526            $routes[] = $this->get(
527                $path . $placeholder,
528                $class . 'show/*',
529                $baseName . '.show'
530            );
531        }
532        if ( ! isset($except['update'])) {
533            $routes[] = $this->patch(
534                $path . $placeholder,
535                $class . 'update/*',
536                $baseName . '.update'
537            );
538        }
539        if ( ! isset($except['replace'])) {
540            $routes[] = $this->put(
541                $path . $placeholder,
542                $class . 'replace/*',
543                $baseName . '.replace'
544            );
545        }
546        if ( ! isset($except['delete'])) {
547            $routes[] = $this->delete(
548                $path . $placeholder,
549                $class . 'delete/*',
550                $baseName . '.delete'
551            );
552        }
553        return $routes;
554    }
555
556    /**
557     * Adds many Routes that can be used by a User Interface.
558     *
559     * @param string $path The URL path
560     * @param string $class The name of the class where the resource will point
561     * @param string $baseName The base name used as a Route name prefix
562     * @param array<int,string> $except Actions not added. Allowed values are:
563     * index, new, create, show, edit, update, remove and delete
564     * @param string $placeholder The placeholder. Normally it matches an id, a number
565     *
566     * @see PresenterInterface
567     * @see Router::$placeholders
568     *
569     * @return array<int,Route> The Routes added to the collection
570     */
571    public function presenter(
572        string $path,
573        string $class,
574        string $baseName,
575        array $except = [],
576        string $placeholder = '{int}'
577    ) : array {
578        $path = \rtrim($path, '/') . '/';
579        $class .= '::';
580        if ($except) {
581            $except = \array_flip($except);
582        }
583        $routes = [];
584        if ( ! isset($except['index'])) {
585            $routes[] = $this->get(
586                $path,
587                $class . 'index/*',
588                $baseName . '.index'
589            );
590        }
591        if ( ! isset($except['new'])) {
592            $routes[] = $this->get(
593                $path . 'new',
594                $class . 'new/*',
595                $baseName . '.new'
596            );
597        }
598        if ( ! isset($except['create'])) {
599            $routes[] = $this->post(
600                $path,
601                $class . 'create/*',
602                $baseName . '.create'
603            );
604        }
605        if ( ! isset($except['show'])) {
606            $routes[] = $this->get(
607                $path . $placeholder,
608                $class . 'show/*',
609                $baseName . '.show'
610            );
611        }
612        if ( ! isset($except['edit'])) {
613            $routes[] = $this->get(
614                $path . $placeholder . '/edit',
615                $class . 'edit/*',
616                $baseName . '.edit'
617            );
618        }
619        if ( ! isset($except['update'])) {
620            $routes[] = $this->post(
621                $path . $placeholder . '/update',
622                $class . 'update/*',
623                $baseName . '.update'
624            );
625        }
626        if ( ! isset($except['remove'])) {
627            $routes[] = $this->get(
628                $path . $placeholder . '/remove',
629                $class . 'remove/*',
630                $baseName . '.remove'
631            );
632        }
633        if ( ! isset($except['delete'])) {
634            $routes[] = $this->post(
635                $path . $placeholder . '/delete',
636                $class . 'delete/*',
637                $baseName . '.delete'
638            );
639        }
640        return $routes;
641    }
642
643    /**
644     * Count routes in the collection.
645     *
646     * @return int
647     */
648    public function count() : int
649    {
650        $count = isset($this->notFoundAction) ? 1 : 0;
651        foreach ($this->routes as $routes) {
652            $count += \count($routes);
653        }
654        return $count;
655    }
656
657    /**
658     * @return array<string,mixed>
659     */
660    public function jsonSerialize() : array
661    {
662        return [
663            'origin' => $this->origin,
664            'routes' => $this->routes,
665            'hasNotFound' => isset($this->notFoundAction),
666        ];
667    }
668}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy