├── phpstan.neon
├── .github
├── FUNDING.yml
└── workflows
│ └── test.yml
├── .editorconfig
├── src
├── Strategy
│ ├── OptionsHandlerInterface.php
│ ├── StrategyAwareInterface.php
│ ├── StrategyAwareTrait.php
│ ├── AbstractStrategy.php
│ ├── StrategyInterface.php
│ ├── ApplicationStrategy.php
│ └── JsonStrategy.php
├── ContainerAwareInterface.php
├── Http
│ ├── Exception
│ │ ├── HttpExceptionInterface.php
│ │ ├── GoneException.php
│ │ ├── ConflictException.php
│ │ ├── ForbiddenException.php
│ │ ├── NotFoundException.php
│ │ ├── BadRequestException.php
│ │ ├── ImATeapotException.php
│ │ ├── UnauthorizedException.php
│ │ ├── LengthRequiredException.php
│ │ ├── NotAcceptableException.php
│ │ ├── TooManyRequestsException.php
│ │ ├── UnsupportedMediaException.php
│ │ ├── ExpectationFailedException.php
│ │ ├── PreconditionFailedException.php
│ │ ├── UnprocessableEntityException.php
│ │ ├── PreconditionRequiredException.php
│ │ ├── UnavailableForLegalReasonsException.php
│ │ └── MethodNotAllowedException.php
│ ├── Request.php
│ ├── Response
│ │ └── Decorator
│ │ │ └── DefaultHeaderDecorator.php
│ └── Exception.php
├── RouteConditionHandlerInterface.php
├── ContainerAwareTrait.php
├── Middleware
│ ├── MiddlewareAwareInterface.php
│ └── MiddlewareAwareTrait.php
├── RouteCollectionInterface.php
├── Cache
│ ├── FileCache.php
│ └── Router.php
├── RouteCollectionTrait.php
├── RouteGroup.php
├── RouteConditionHandlerTrait.php
├── Route.php
├── Dispatcher.php
└── Router.php
├── phpcs.xml
├── phpunit.xml
├── LICENSE.md
├── CONTRIBUTING.md
├── composer.json
├── README.md
└── CHANGELOG.md
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 4
3 | paths:
4 | - src
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [philipobenito]
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | max_line_length = 120
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/src/Strategy/OptionsHandlerInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | src
14 | tests
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Strategy/StrategyAwareTrait.php:
--------------------------------------------------------------------------------
1 | strategy = $strategy;
14 | return $this;
15 | }
16 |
17 | public function getStrategy(): ?StrategyInterface
18 | {
19 | return $this->strategy;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Http/Exception/UnavailableForLegalReasonsException.php:
--------------------------------------------------------------------------------
1 | implode(', ', $allowed)
20 | ];
21 |
22 | parent::__construct(405, $message, $previous, $headers, $code);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Strategy/AbstractStrategy.php:
--------------------------------------------------------------------------------
1 | responseDecorators[] = $decorator;
16 | return $this;
17 | }
18 |
19 | protected function decorateResponse(ResponseInterface $response): ResponseInterface
20 | {
21 | foreach ($this->responseDecorators as $decorator) {
22 | $response = $decorator($response);
23 | }
24 |
25 | return $response;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Strategy/StrategyInterface.php:
--------------------------------------------------------------------------------
1 | container;
17 | }
18 |
19 | public function setContainer(ContainerInterface $container): ContainerAwareInterface
20 | {
21 | $this->container = $container;
22 |
23 | if ($this instanceof ContainerAwareInterface) {
24 | return $this;
25 | }
26 |
27 | throw new RuntimeException(sprintf(
28 | 'Trait (%s) must be consumed by an instance of (%s)',
29 | __TRAIT__,
30 | ContainerAwareInterface::class
31 | ));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Middleware/MiddlewareAwareInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | tests
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | src/
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/RouteCollectionInterface.php:
--------------------------------------------------------------------------------
1 | addDefaultHeaders($headers);
16 | }
17 |
18 | public function __invoke(ResponseInterface $response): ResponseInterface
19 | {
20 | foreach ($this->headers as $name => $value) {
21 | if (false === $response->hasHeader($name)) {
22 | $response = $response->withAddedHeader($name, $value);
23 | }
24 | }
25 |
26 | return $response;
27 | }
28 |
29 | public function addDefaultHeader(string $name, string $value): self
30 | {
31 | $this->headers[$name] = $value;
32 | return $this;
33 | }
34 |
35 | public function addDefaultHeaders(array $headers): self
36 | {
37 | foreach ($headers as $name => $value) {
38 | $this->addDefaultHeader($name, $value);
39 | }
40 |
41 | return $this;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Phil Bennett
4 |
5 | > Permission is hereby granted, free of charge, to any person obtaining a copy
6 | > of this software and associated documentation files (the "Software"), to deal
7 | > in the Software without restriction, including without limitation the rights
8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | > copies of the Software, and to permit persons to whom the Software is
10 | > furnished to do so, subject to the following conditions:
11 | >
12 | > The above copyright notice and this permission notice shall be included in
13 | > all copies or substantial portions of the Software.
14 | >
15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | > THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are **welcome** and will be fully **credited**.
4 |
5 | We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/route).
6 |
7 |
8 | ## Pull Requests
9 |
10 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
11 |
12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
13 |
14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
15 |
16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
17 |
18 | - **Create feature branches** - Don't ask us to pull from your master branch.
19 |
20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
21 |
22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
23 |
24 |
25 | ## Running Tests
26 |
27 | ``` bash
28 | $ vendor/bin/phpunit
29 | ```
30 |
31 |
32 | **Happy coding**!
33 |
--------------------------------------------------------------------------------
/src/Http/Exception.php:
--------------------------------------------------------------------------------
1 | message, $code, $previous);
20 | }
21 |
22 | public function getStatusCode(): int
23 | {
24 | return $this->status;
25 | }
26 |
27 | public function getHeaders(): array
28 | {
29 | return $this->headers;
30 | }
31 |
32 | public function buildJsonResponse(ResponseInterface $response): ResponseInterface
33 | {
34 | $this->headers['content-type'] = 'application/json';
35 |
36 | foreach ($this->headers as $key => $value) {
37 | /** @var ResponseInterface $response */
38 | $response = $response->withAddedHeader($key, $value);
39 | }
40 |
41 | if ($response->getBody()->isWritable()) {
42 | $response->getBody()->write(json_encode([
43 | 'status_code' => $this->status,
44 | 'reason_phrase' => $this->message
45 | ]));
46 | }
47 |
48 | return $response->withStatus($this->status, $this->message);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Cache/FileCache.php:
--------------------------------------------------------------------------------
1 | has($key)) ? file_get_contents($this->cacheFilePath) : $default;
18 | }
19 |
20 | public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool
21 | {
22 | return (bool) file_put_contents($this->cacheFilePath, $value);
23 | }
24 |
25 | public function has(string $key): bool
26 | {
27 | return file_exists($this->cacheFilePath) && time() - filemtime($this->cacheFilePath) < $this->ttl;
28 | }
29 |
30 | public function delete(string $key): bool
31 | {
32 | return unlink($this->cacheFilePath);
33 | }
34 |
35 | public function clear(): bool
36 | {
37 | return $this->delete($this->cacheFilePath);
38 | }
39 |
40 | public function getMultiple(iterable $keys, mixed $default = null): iterable
41 | {
42 | return [];
43 | }
44 |
45 | public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool
46 | {
47 | return false;
48 | }
49 |
50 | public function deleteMultiple(iterable $keys): bool
51 | {
52 | return false;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/RouteCollectionTrait.php:
--------------------------------------------------------------------------------
1 | map(Request::METHOD_DELETE, $path, $handler);
21 | }
22 |
23 | public function get(string $path, callable|array|string|RequestHandlerInterface $handler): Route
24 | {
25 | return $this->map(Request::METHOD_GET, $path, $handler);
26 | }
27 |
28 | public function head(string $path, callable|array|string|RequestHandlerInterface $handler): Route
29 | {
30 | return $this->map(Request::METHOD_HEAD, $path, $handler);
31 | }
32 |
33 | public function options(string $path, callable|array|string|RequestHandlerInterface $handler): Route
34 | {
35 | return $this->map(Request::METHOD_OPTIONS, $path, $handler);
36 | }
37 |
38 | public function patch(string $path, callable|array|string|RequestHandlerInterface $handler): Route
39 | {
40 | return $this->map(Request::METHOD_PATCH, $path, $handler);
41 | }
42 |
43 | public function post(string $path, callable|array|string|RequestHandlerInterface $handler): Route
44 | {
45 | return $this->map(Request::METHOD_POST, $path, $handler);
46 | }
47 |
48 | public function put(string $path, callable|array|string|RequestHandlerInterface $handler): Route
49 | {
50 | return $this->map(Request::METHOD_PUT, $path, $handler);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/RouteGroup.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
33 | $this->prefix = sprintf('/%s', ltrim($this->prefix, '/'));
34 | }
35 |
36 | public function __invoke(): void
37 | {
38 | ($this->callback)($this);
39 | }
40 |
41 | public function getPrefix(): string
42 | {
43 | return $this->prefix;
44 | }
45 |
46 | public function map(
47 | string|array $method,
48 | string $path,
49 | callable|array|string|RequestHandlerInterface $handler
50 | ): Route {
51 | $path = ($path === '/') ? $this->prefix : $this->prefix . sprintf('/%s', ltrim($path, '/'));
52 | $route = $this->collection->map($method, $path, $handler);
53 |
54 | $route->setParentGroup($this);
55 |
56 | if ($host = $this->getHost()) {
57 | $route->setHost($host);
58 | }
59 |
60 | if ($scheme = $this->getScheme()) {
61 | $route->setScheme($scheme);
62 | }
63 |
64 | if ($port = $this->getPort()) {
65 | $route->setPort($port);
66 | }
67 |
68 | if ($route->getStrategy() === null && $this->getStrategy() !== null) {
69 | $route->setStrategy($this->getStrategy());
70 | }
71 |
72 | return $route;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "league/route",
3 | "description": "Fast routing and dispatch component including PSR-15 middleware, built on top of FastRoute.",
4 | "keywords": [
5 | "league",
6 | "route",
7 | "router",
8 | "dispatcher",
9 | "psr-7",
10 | "psr7",
11 | "psr-15",
12 | "psr15"
13 | ],
14 | "homepage": "https://github.com/thephpleague/route",
15 | "license": "MIT",
16 | "authors": [
17 | {
18 | "name": "Phil Bennett",
19 | "email": "mail@philbennett.co.uk",
20 | "role": "Developer"
21 | }
22 | ],
23 | "require": {
24 | "php": "^8.1",
25 | "laravel/serializable-closure": "^2.0.0",
26 | "nikic/fast-route": "^1.3",
27 | "psr/container": "^2.0",
28 | "psr/http-factory": "^1.0",
29 | "psr/http-message": "^2.0",
30 | "psr/http-server-handler": "^1.0.1",
31 | "psr/http-server-middleware": "^1.0.1",
32 | "psr/simple-cache": "^3.0"
33 | },
34 | "require-dev": {
35 | "laminas/laminas-diactoros": "^3.5",
36 | "phpstan/phpstan": "^1.12",
37 | "phpstan/phpstan-phpunit": "^1.3",
38 | "phpunit/phpunit": "^10.2",
39 | "roave/security-advisories": "dev-latest",
40 | "scrutinizer/ocular": "^1.8",
41 | "squizlabs/php_codesniffer": "^3.7"
42 | },
43 | "replace": {
44 | "orno/route": "~1.0",
45 | "orno/http": "~1.0"
46 | },
47 | "autoload": {
48 | "psr-4": {
49 | "League\\Route\\": "src"
50 | }
51 | },
52 | "autoload-dev": {
53 | "psr-4": {
54 | "League\\Route\\": "tests"
55 | },
56 | "files": ["tests/Fixture/function.php"]
57 | },
58 | "extra": {
59 | "branch-alias": {
60 | "dev-master": "6.x-dev",
61 | "dev-6.x": "6.x-dev",
62 | "dev-5.x": "5.x-dev",
63 | "dev-4.x": "4.x-dev",
64 | "dev-3.x": "3.x-dev",
65 | "dev-2.x": "2.x-dev",
66 | "dev-1.x": "1.x-dev"
67 | }
68 | },
69 | "scripts": {
70 | "analyse": "vendor/bin/phpstan analyse -l 4 --no-progress src",
71 | "check": "vendor/bin/phpunit && vendor/bin/phpstan analyse -l 4 --no-progress src",
72 | "test": "vendor/bin/phpunit"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push: ~
5 | pull_request: ~
6 |
7 | jobs:
8 | phpcs:
9 | name: PHPCS
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - uses: shivammathur/setup-php@v2
16 | with:
17 | php-version: 8.4
18 | extensions: curl, mbstring
19 | coverage: none
20 | tools: composer:v2, cs2pr
21 |
22 | - run: composer update --no-progress
23 | - run: vendor/bin/phpcs -q --report=checkstyle | cs2pr
24 |
25 | phpunit:
26 | name: PHPUnit on ${{ matrix.php }} ${{ matrix.composer-flags }}
27 | runs-on: ubuntu-latest
28 | strategy:
29 | matrix:
30 | php: ['8.1', '8.2', '8.3', '8.4']
31 | coverage: [true]
32 | composer-flags: ['']
33 |
34 | steps:
35 | - uses: actions/checkout@v2
36 | with:
37 | fetch-depth: 0
38 |
39 | - uses: shivammathur/setup-php@v2
40 | with:
41 | php-version: ${{ matrix.php }}
42 | extensions: curl, mbstring
43 | coverage: pcov
44 | tools: composer:v2
45 |
46 | # - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
47 |
48 | - run: composer update --no-progress ${{ matrix.composer-flags }}
49 |
50 | - run: vendor/bin/phpunit --no-coverage
51 | if: ${{ !matrix.coverage }}
52 |
53 | - run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
54 | if: ${{ matrix.coverage }}
55 |
56 | - run: php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover
57 | if: ${{ matrix.coverage }}
58 |
59 | phpstan:
60 | name: PHPStan
61 | runs-on: ubuntu-latest
62 |
63 | steps:
64 | - uses: actions/checkout@v2
65 |
66 | - uses: shivammathur/setup-php@v2
67 | with:
68 | php-version: 8.4
69 | extensions: curl, mbstring
70 | coverage: none
71 | tools: composer:v2
72 |
73 | - run: composer update --no-progress
74 | - run: vendor/bin/phpstan analyse --no-progress
75 |
--------------------------------------------------------------------------------
/src/Strategy/ApplicationStrategy.php:
--------------------------------------------------------------------------------
1 | throwThrowableMiddleware($exception);
21 | }
22 |
23 | public function getNotFoundDecorator(NotFoundException $exception): MiddlewareInterface
24 | {
25 | return $this->throwThrowableMiddleware($exception);
26 | }
27 |
28 | public function getThrowableHandler(): MiddlewareInterface
29 | {
30 | return new class implements MiddlewareInterface
31 | {
32 | public function process(
33 | ServerRequestInterface $request,
34 | RequestHandlerInterface $handler
35 | ): ResponseInterface {
36 | try {
37 | return $handler->handle($request);
38 | } catch (Throwable $e) {
39 | throw $e;
40 | }
41 | }
42 | };
43 | }
44 |
45 | public function invokeRouteCallable(Route $route, ServerRequestInterface $request): ResponseInterface
46 | {
47 | $controller = $route->getCallable($this->getContainer());
48 | $response = $controller($request, $route->getVars());
49 | return $this->decorateResponse($response);
50 | }
51 |
52 | protected function throwThrowableMiddleware(Throwable $error): MiddlewareInterface
53 | {
54 | return new class ($error) implements MiddlewareInterface
55 | {
56 | protected $error;
57 |
58 | public function __construct(Throwable $error)
59 | {
60 | $this->error = $error;
61 | }
62 |
63 | public function process(
64 | ServerRequestInterface $request,
65 | RequestHandlerInterface $handler
66 | ): ResponseInterface {
67 | throw $this->error;
68 | }
69 | };
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Cache/Router.php:
--------------------------------------------------------------------------------
1 | cacheEnabled && $builder instanceof \Closure) {
35 | $builder = new SerializableClosure($builder);
36 | }
37 |
38 | $this->builder = $builder;
39 | }
40 |
41 | /**
42 | * @throws \Psr\SimpleCache\InvalidArgumentException
43 | */
44 | public function dispatch(ServerRequestInterface $request): ResponseInterface
45 | {
46 | $router = $this->buildRouter($request);
47 | return $router->dispatch($request);
48 | }
49 |
50 | /**
51 | * @throws \Psr\SimpleCache\InvalidArgumentException
52 | */
53 | protected function buildRouter(ServerRequestInterface $request): MainRouter
54 | {
55 | if (true === $this->cacheEnabled && $cache = $this->cache->get($this->cacheKey)) {
56 | $router = unserialize($cache, ['allowed_classes' => true]);
57 |
58 | if ($router instanceof MainRouter) {
59 | return $router;
60 | }
61 | }
62 |
63 | $builder = $this->builder;
64 |
65 | if ($builder instanceof SerializableClosure) {
66 | $builder = $builder->getClosure();
67 | }
68 |
69 | $router = $builder(new MainRouter());
70 |
71 | if (false === $this->cacheEnabled) {
72 | return $router;
73 | }
74 |
75 | if ($router instanceof MainRouter) {
76 | $router->prepareRoutes($request);
77 | $this->cache->set($this->cacheKey, serialize($router));
78 | return $router;
79 | }
80 |
81 | throw new InvalidArgumentException('Invalid Router builder provided to cached router');
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/RouteConditionHandlerTrait.php:
--------------------------------------------------------------------------------
1 | host;
20 | }
21 |
22 | public function getName(): ?string
23 | {
24 | return $this->name;
25 | }
26 |
27 | public function getPort(): ?int
28 | {
29 | return $this->port;
30 | }
31 |
32 | public function getScheme(): ?string
33 | {
34 | return $this->scheme;
35 | }
36 |
37 | public function setHost(string $host): RouteConditionHandlerInterface
38 | {
39 | $this->host = $host;
40 | return $this->checkAndReturnSelf();
41 | }
42 |
43 | public function setName(string $name): RouteConditionHandlerInterface
44 | {
45 | $this->name = $name;
46 | return $this->checkAndReturnSelf();
47 | }
48 |
49 | public function setPort(int $port): RouteConditionHandlerInterface
50 | {
51 | $this->port = $port;
52 | return $this->checkAndReturnSelf();
53 | }
54 |
55 | public function setScheme(string $scheme): RouteConditionHandlerInterface
56 | {
57 | $this->scheme = $scheme;
58 | return $this->checkAndReturnSelf();
59 | }
60 |
61 | private function checkAndReturnSelf(): RouteConditionHandlerInterface
62 | {
63 | if ($this instanceof RouteConditionHandlerInterface) {
64 | return $this;
65 | }
66 |
67 | throw new RuntimeException(sprintf(
68 | 'Trait (%s) must be consumed by an instance of (%s)',
69 | __TRAIT__,
70 | RouteConditionHandlerInterface::class
71 | ));
72 | }
73 |
74 | protected function isExtraConditionMatch(Route $route, ServerRequestInterface $request): bool
75 | {
76 | // check for scheme condition
77 | $scheme = $route->getScheme();
78 | if ($scheme !== null && $scheme !== $request->getUri()->getScheme()) {
79 | return false;
80 | }
81 |
82 | // check for domain condition
83 | $host = $route->getHost();
84 | if ($host !== null && $host !== $request->getUri()->getHost()) {
85 | return false;
86 | }
87 |
88 | // check for port condition
89 | $port = $route->getPort();
90 | return !($port !== null && $port !== $request->getUri()->getPort());
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Middleware/MiddlewareAwareTrait.php:
--------------------------------------------------------------------------------
1 | middleware;
22 | }
23 |
24 | public function lazyMiddleware(string $middleware): MiddlewareAwareInterface
25 | {
26 | $this->middleware[] = $middleware;
27 | return $this;
28 | }
29 |
30 | public function lazyMiddlewares(array $middlewares): MiddlewareAwareInterface
31 | {
32 | foreach ($middlewares as $middleware) {
33 | $this->lazyMiddleware($middleware);
34 | }
35 |
36 | return $this;
37 | }
38 |
39 | public function lazyPrependMiddleware(string $middleware): MiddlewareAwareInterface
40 | {
41 | array_unshift($this->middleware, $middleware);
42 | return $this;
43 | }
44 |
45 | public function middleware(MiddlewareInterface $middleware): MiddlewareAwareInterface
46 | {
47 | $this->middleware[] = $middleware;
48 | return $this;
49 | }
50 |
51 | public function middlewares(array $middlewares): MiddlewareAwareInterface
52 | {
53 | foreach ($middlewares as $middleware) {
54 | $this->middleware($middleware);
55 | }
56 |
57 | return $this;
58 | }
59 |
60 | public function prependMiddleware(MiddlewareInterface $middleware): MiddlewareAwareInterface
61 | {
62 | array_unshift($this->middleware, $middleware);
63 | return $this;
64 | }
65 |
66 | public function shiftMiddleware(): MiddlewareInterface
67 | {
68 | $middleware = array_shift($this->middleware);
69 |
70 | if ($middleware === null) {
71 | throw new OutOfBoundsException('Reached end of middleware stack. Does your controller return a response?');
72 | }
73 |
74 | return $middleware;
75 | }
76 |
77 | protected function resolveMiddleware($middleware, ?ContainerInterface $container = null): MiddlewareInterface
78 | {
79 | if ($container === null && is_string($middleware) && class_exists($middleware)) {
80 | $middleware = new $middleware();
81 | }
82 |
83 | if ($container !== null && is_string($middleware) && $container->has($middleware)) {
84 | $middleware = $container->get($middleware);
85 | }
86 |
87 | if ($middleware instanceof MiddlewareInterface) {
88 | return $middleware;
89 | }
90 |
91 | throw new InvalidArgumentException(sprintf('Could not resolve middleware class: %s', $middleware));
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Route
2 |
3 | [](https://github.com/philipobenito)
4 | [](https://github.com/thephpleague/route/releases)
5 | [](LICENSE.md)
6 | [](https://github.com/thephpleague/route/actions/workflows/test.yml)
7 | [](https://scrutinizer-ci.com/g/thephpleague/route/code-structure)
8 | [](https://scrutinizer-ci.com/g/thephpleague/route)
9 | [](https://packagist.org/packages/league/route)
10 |
11 | This package is compliant with [PSR-1], [PSR-2], [PSR-4], [PSR-7], [PSR-11], [PSR-12] and [PSR-15]. If you notice compliance oversights, please send a patch via pull request.
12 |
13 | [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md
14 | [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
15 | [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md
16 | [PSR-7]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md
17 | [PSR-11]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md
18 | [PSR-12]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md
19 | [PSR-15]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-15-request-handlers.md
20 |
21 | ## Install
22 |
23 | Via Composer
24 |
25 | ``` bash
26 | $ composer require league/route
27 | ```
28 |
29 | ## Requirements
30 |
31 | The following versions of PHP are supported by this version.
32 |
33 | * PHP 8.1
34 | * PHP 8.2
35 | * PHP 8.3
36 | * PHP 8.4
37 |
38 | ## Documentation
39 |
40 | Route has [full documentation](http://route.thephpleague.com), powered by [Jekyll](http://jekyllrb.com/).
41 |
42 | Contribute to this documentation in the [docs directory](https://github.com/thephpleague/route/tree/master/docs/).
43 |
44 | ## Testing
45 |
46 | ``` bash
47 | $ vendor/bin/phpunit
48 | ```
49 |
50 | ## Contributing
51 |
52 | Please see [CONTRIBUTING](https://github.com/thephpleague/route/blob/master/CONTRIBUTING.md) for details.
53 |
54 | ## Credits
55 |
56 | - [Phil Bennett](https://github.com/philipobenito)
57 | - [Nikita Popov](https://github.com/nikic) ([FastRoute](https://github.com/nikic/FastRoute))
58 | - [All Contributors](https://github.com/thephpleague/route/contributors)
59 |
60 | ## License
61 |
62 | The MIT License (MIT). Please see [License File](https://github.com/thephpleague/route/blob/master/LICENSE.md) for more information.
63 |
--------------------------------------------------------------------------------
/src/Strategy/JsonStrategy.php:
--------------------------------------------------------------------------------
1 | addResponseDecorator(static function (ResponseInterface $response): ResponseInterface {
23 | if (false === $response->hasHeader('content-type')) {
24 | $response = $response->withHeader('content-type', 'application/json');
25 | }
26 |
27 | return $response;
28 | });
29 | }
30 |
31 | public function getMethodNotAllowedDecorator(MethodNotAllowedException $exception): MiddlewareInterface
32 | {
33 | return $this->buildJsonResponseMiddleware($exception);
34 | }
35 |
36 | public function getNotFoundDecorator(NotFoundException $exception): MiddlewareInterface
37 | {
38 | return $this->buildJsonResponseMiddleware($exception);
39 | }
40 |
41 | public function getOptionsCallable(array $methods): callable
42 | {
43 | return function () use ($methods): ResponseInterface {
44 | $options = implode(', ', $methods);
45 | $response = $this->responseFactory->createResponse();
46 | $response = $response->withHeader('allow', $options);
47 | return $response->withHeader('access-control-allow-methods', $options);
48 | };
49 | }
50 |
51 | public function getThrowableHandler(): MiddlewareInterface
52 | {
53 | return new class ($this->responseFactory->createResponse()) implements MiddlewareInterface
54 | {
55 | protected $response;
56 |
57 | public function __construct(ResponseInterface $response)
58 | {
59 | $this->response = $response;
60 | }
61 |
62 | public function process(
63 | ServerRequestInterface $request,
64 | RequestHandlerInterface $handler
65 | ): ResponseInterface {
66 | try {
67 | return $handler->handle($request);
68 | } catch (Throwable $exception) {
69 | $response = $this->response;
70 |
71 | if ($exception instanceof Http\Exception) {
72 | return $exception->buildJsonResponse($response);
73 | }
74 |
75 | $response->getBody()->write(json_encode([
76 | 'status_code' => 500,
77 | 'reason_phrase' => $exception->getMessage()
78 | ]));
79 |
80 | $response = $response->withAddedHeader('content-type', 'application/json');
81 | return $response->withStatus(500, strtok($exception->getMessage(), "\n"));
82 | }
83 | }
84 | };
85 | }
86 |
87 | public function invokeRouteCallable(Route $route, ServerRequestInterface $request): ResponseInterface
88 | {
89 | $controller = $route->getCallable($this->getContainer());
90 | $response = $controller($request, $route->getVars());
91 |
92 | if ($this->isJsonSerializable($response)) {
93 | $body = json_encode($response, $this->jsonFlags);
94 | $response = $this->responseFactory->createResponse();
95 | $response->getBody()->write($body);
96 | }
97 |
98 | return $this->decorateResponse($response);
99 | }
100 |
101 | protected function buildJsonResponseMiddleware(Http\Exception $exception): MiddlewareInterface
102 | {
103 | return new class ($this->responseFactory->createResponse(), $exception) implements MiddlewareInterface
104 | {
105 | protected $response;
106 | protected $exception;
107 |
108 | public function __construct(ResponseInterface $response, Http\Exception $exception)
109 | {
110 | $this->response = $response;
111 | $this->exception = $exception;
112 | }
113 |
114 | public function process(
115 | ServerRequestInterface $request,
116 | RequestHandlerInterface $handler
117 | ): ResponseInterface {
118 | return $this->exception->buildJsonResponse($this->response);
119 | }
120 | };
121 | }
122 |
123 | protected function isJsonSerializable($response): bool
124 | {
125 | if ($response instanceof ResponseInterface) {
126 | return false;
127 | }
128 |
129 | return (is_array($response) || is_object($response) || $response instanceof JsonSerializable);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/Route.php:
--------------------------------------------------------------------------------
1 | |string $method
31 | * @param array $vars
32 | */
33 | public function __construct(
34 | protected array|string $method,
35 | protected string $path,
36 | callable|array|string|RequestHandlerInterface $handler,
37 | protected ?RouteGroup $group = null,
38 | protected array $vars = []
39 | ) {
40 | if ($handler instanceof \Closure) {
41 | $handler = new SerializableClosure($handler);
42 | }
43 |
44 | $this->handler = ($handler instanceof RequestHandlerInterface) ? [$handler, 'handle'] : $handler;
45 | }
46 |
47 | public function getCallable(?ContainerInterface $container = null): callable
48 | {
49 | $callable = $this->handler;
50 |
51 | if ($callable instanceof SerializableClosure) {
52 | $callable = $callable->getClosure();
53 | }
54 |
55 | if (is_string($callable) && str_contains($callable, '::')) {
56 | $callable = explode('::', $callable);
57 | }
58 |
59 | if (is_array($callable) && isset($callable[0]) && is_object($callable[0])) {
60 | $callable = [$callable[0], $callable[1]];
61 | }
62 |
63 | if (is_array($callable) && isset($callable[0]) && is_string($callable[0])) {
64 | $callable = [$this->resolve($callable[0], $container), $callable[1]];
65 | }
66 |
67 | if (is_string($callable)) {
68 | $callable = $this->resolve($callable, $container);
69 | }
70 |
71 | if ($callable instanceof RequestHandlerInterface) {
72 | $callable = [$callable, 'handle'];
73 | }
74 |
75 | if (!is_callable($callable)) {
76 | throw new RuntimeException('Could not resolve a callable for this route');
77 | }
78 |
79 | return $callable;
80 | }
81 |
82 | /**
83 | * @return array|string
84 | */
85 | public function getMethod(): array|string
86 | {
87 | return $this->method;
88 | }
89 |
90 | public function getParentGroup(): ?RouteGroup
91 | {
92 | return $this->group;
93 | }
94 |
95 | public function getPath(array $replacements = []): string
96 | {
97 | $toReplace = [];
98 |
99 | foreach ($replacements as $wildcard => $actual) {
100 | $toReplace['/{' . preg_quote($wildcard, '/') . '(:.*?)?}/'] = $actual;
101 | }
102 |
103 | return preg_replace(array_keys($toReplace), array_values($toReplace), $this->path);
104 | }
105 |
106 | /**
107 | * @return array
108 | */
109 | public function getVars(): array
110 | {
111 | return $this->vars;
112 | }
113 |
114 | public function process(
115 | ServerRequestInterface $request,
116 | RequestHandlerInterface $handler
117 | ): ResponseInterface {
118 | $strategy = $this->getStrategy();
119 |
120 | if (!($strategy instanceof StrategyInterface)) {
121 | throw new RuntimeException('A strategy must be set to process a route');
122 | }
123 |
124 | return $strategy->invokeRouteCallable($this, $request);
125 | }
126 |
127 | public function setParentGroup(RouteGroup $group): self
128 | {
129 | $this->group = $group;
130 | $prefix = $this->group->getPrefix();
131 | $path = $this->getPath();
132 |
133 | if (strcmp($prefix, substr($path, 0, strlen($prefix))) !== 0) {
134 | $path = $prefix . $path;
135 | $this->path = $path;
136 | }
137 |
138 | return $this;
139 | }
140 |
141 | /**
142 | * @param array $vars
143 | */
144 | public function setVars(array $vars): self
145 | {
146 | $this->vars = $vars;
147 | return $this;
148 | }
149 |
150 | /**
151 | * @throws ContainerExceptionInterface
152 | * @throws NotFoundExceptionInterface
153 | */
154 | protected function resolve(string $class, ?ContainerInterface $container = null): mixed
155 | {
156 | if ($container instanceof ContainerInterface && $container->has($class)) {
157 | return $container->get($class);
158 | }
159 |
160 | if (class_exists($class)) {
161 | return new $class();
162 | }
163 |
164 | return $class;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/Dispatcher.php:
--------------------------------------------------------------------------------
1 | getMethod();
29 | $uri = $request->getUri()->getPath();
30 | $match = $this->dispatch($method, $uri);
31 |
32 | switch ($match[0]) {
33 | case FastRoute::NOT_FOUND:
34 | $this->setNotFoundDecoratorMiddleware();
35 | break;
36 | case FastRoute::METHOD_NOT_ALLOWED:
37 | $allowed = (array) $match[1];
38 | $this->setMethodNotAllowedDecoratorMiddleware($allowed);
39 | break;
40 | case FastRoute::FOUND:
41 | $route = $this->ensureHandlerIsRoute($match[1], $method, $uri)->setVars($match[2]);
42 |
43 | if ($this->isExtraConditionMatch($route, $request)) {
44 | $this->setFoundMiddleware($route);
45 | $request = $this->requestWithRouteAttributes($request, $route);
46 | break;
47 | }
48 |
49 | $this->setNotFoundDecoratorMiddleware();
50 | break;
51 | }
52 |
53 | return $this->handle($request);
54 | }
55 |
56 | public function handle(ServerRequestInterface $request): ResponseInterface
57 | {
58 | $middleware = $this->shiftMiddleware();
59 | return $middleware->process($request, $this);
60 | }
61 |
62 | protected function ensureHandlerIsRoute($matchingHandler, $httpMethod, $uri): Route
63 | {
64 | if ($matchingHandler instanceof Route) {
65 | return $matchingHandler;
66 | }
67 |
68 | return new Route($httpMethod, $uri, $matchingHandler);
69 | }
70 |
71 | protected function requestWithRouteAttributes(ServerRequestInterface $request, Route $route): ServerRequestInterface
72 | {
73 | $routerParams = $route->getVars();
74 |
75 | foreach ($routerParams as $key => $value) {
76 | $request = $request->withAttribute($key, $value);
77 | }
78 |
79 | return $request;
80 | }
81 |
82 | protected function setFoundMiddleware(Route $route): void
83 | {
84 | if ($route->getStrategy() === null) {
85 | $strategy = $this->getStrategy();
86 |
87 | if (!($strategy instanceof StrategyInterface)) {
88 | throw new RuntimeException('Cannot determine strategy to use for dispatch of found route');
89 | }
90 |
91 | $route->setStrategy($strategy);
92 | }
93 |
94 | $strategy = $route->getStrategy();
95 | $container = $strategy instanceof ContainerAwareInterface ? $strategy->getContainer() : null;
96 |
97 | foreach ($this->getMiddlewareStack() as $key => $middleware) {
98 | $this->middleware[$key] = $this->resolveMiddleware($middleware, $container);
99 | }
100 |
101 | // wrap entire dispatch process in exception handler
102 | $this->prependMiddleware($strategy->getThrowableHandler());
103 |
104 | // add group and route specific middleware
105 | if ($group = $route->getParentGroup()) {
106 | foreach ($group->getMiddlewareStack() as $middleware) {
107 | $this->middleware($this->resolveMiddleware($middleware, $container));
108 | }
109 | }
110 |
111 | foreach ($route->getMiddlewareStack() as $middleware) {
112 | $this->middleware($this->resolveMiddleware($middleware, $container));
113 | }
114 |
115 | // add actual route to end of stack
116 | $this->middleware($route);
117 | }
118 |
119 | protected function setMethodNotAllowedDecoratorMiddleware(array $allowed): void
120 | {
121 | $strategy = $this->getStrategy();
122 |
123 | if (!($strategy instanceof StrategyInterface)) {
124 | throw new RuntimeException('Cannot determine strategy to use for dispatch of method not allowed route');
125 | }
126 |
127 | $middleware = $strategy->getMethodNotAllowedDecorator(new MethodNotAllowedException($allowed));
128 | $this->prependMiddleware($middleware);
129 | }
130 |
131 | protected function setNotFoundDecoratorMiddleware(): void
132 | {
133 | $strategy = $this->getStrategy();
134 |
135 | if (!($strategy instanceof StrategyInterface)) {
136 | throw new RuntimeException('Cannot determine strategy to use for dispatch of not found route');
137 | }
138 |
139 | $middleware = $strategy->getNotFoundDecorator(new NotFoundException());
140 | $this->prependMiddleware($middleware);
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
6 |
7 | ## [6.2.0] 2024-11
8 |
9 | ### Changed
10 | - Replaced opis/closure with laravel/serializable-closure and implemented throughout the handler process rather than a blanket serialisation of the router.
11 |
12 | ## [6.1.1] 2024-11
13 |
14 | ### Fixed
15 | - Further fixes for type hinting bug related to array based callables with a string class name.
16 |
17 | ## [6.1.0] 2024-11
18 |
19 | ### Fixed
20 | - Fixed a bug introduced in 6.0.0 where an array based callable with a string class name would not be considered valid.
21 | - Added some doc comments for clarity on array types. (@marekskopal)
22 |
23 | ### Changed
24 | - Updated `psr/http-message` to `^2.0.0`.
25 |
26 | ## [6.0.0] 2024-11
27 |
28 | > Note: While this is a major release, there are no breaking changes to the public API. The major version bump is due to the removal of support for PHP 8.0 and below.
29 | >
30 | > This being said, there are some internal changes that may affect you if you have extended the library in any way. Please test thoroughly before upgrading.
31 |
32 | ### Added
33 | - Added full support for PHP 8.1 to 8.4.
34 | - Ability to use a PSR-15 middleware as a controller.
35 | - Ability to pass an array of HTTP methods to `Router::map` to create a route that matches multiple methods.
36 | - This method still accepts a string so is not a breaking change.
37 | - Ability to add a custom key to a caching router.
38 |
39 | ### Changed
40 | - Fixes and improvements throughout for PHP 8.1 to 8.4.
41 |
42 | ### Removed
43 | - Removed support for PHP < 8.1.
44 |
45 | ## [5.1.0] 2021-07
46 |
47 | ### Added
48 | - Support for named routes within groups (@Fredrik82)
49 |
50 | ## [5.0.1] 2021-03
51 |
52 | ### Added
53 | - Support for `psr/container:2.0`
54 |
55 | ## [5.0.0] 2021-01
56 |
57 | ### Added
58 | - A cached router, a way to have a fully built router cached and resolved from cache on subsequent requests.
59 | - Response decorators, a way to manipulate a response object returned from a matched route.
60 | - Automatic generation of OPTIONS routes if they have not been defined.
61 |
62 | ### Changed
63 | - Minimum PHP requirement bumped to 7.2.
64 | - `Router` no longer extends FastRoute `RouteCollecter`.
65 | - `Router` constructor no longer accepts optional FastRoute `RouteParser` and `DataGenerator`.
66 | - `Router` constructor now accepts an optional FastRoute `RouteCollector`.
67 | - Routes already registered with FastRoute `RouteCollector` are respected and matched.
68 | - Separated route preparation from dispatch process so that the router can dispatch multiple times.
69 | - General code improvements.
70 |
71 | ### Removed
72 | - Setting of default response headers on strategies. (Replaced by response decorators, see Added).
73 | - Exception handlers from strategies. (Already deprecated in favour of throwable handlers).
74 |
75 | ## [4.5.1] 2021.01
76 |
77 | ### Added
78 | - Official support for PHP 8.0.
79 |
80 | ## [4.5.0] 2020-05
81 |
82 | ### Added
83 | - Ability to pass optional `$replacements` array to `Route::getPath` in order to build literal route path strings.
84 |
85 | ## [4.4.0] 2020-05
86 |
87 | ### Added
88 | - Ability to pass JSON flags to JsonStrategy. (@pine3ree)
89 | - Router is now a RequestHandlerInterface so can be used as a middleware itself. (@delboy1978uk)
90 | - Route params now added as Request attributes. (@delboy1978uk)
91 |
92 | ### Fixed
93 | - Exception moved to more appropriate place when shifting no middleware. (@delboy1978uk)
94 | - Ensure group prefix is always added when adding a parent group. (@delboy1978uk)
95 |
96 |
97 | ## [4.3.1] 2019-07
98 |
99 | ### Fixed
100 | - Fixed bug when attempting to get a container for custom strategy that is not container aware.
101 |
102 | ## [4.3.0] 2019-06
103 |
104 | ### Added
105 | - Ability to add middleware to the stack as a class name so it is only instantiated when used.
106 |
107 | ### Changed
108 | - Switch to use `zendframework/zend-httphandlerrunner` as removed from `diactoros` (@JohnstonCode)
109 |
110 | ### Fixed
111 | - When adding a prefix to a group after adding routes, it is now applied to those routes. (@delboy1978uk)
112 | - Fix to how shifting middleware is handled to prevent error triggering. (@delboy1978uk)
113 | - Fix to ensure that when invoking FastRoute methods on League\Route all callables are converted to League\Route objects (@pgk)
114 | - Various documentation fixes.
115 |
116 | ## [4.2.0] 2018-10
117 |
118 | ### Added
119 | - Allow adding default response headers to strategies.
120 | - Expand error handling to include Throwable.
121 |
122 | ## [4.1.1] 2018-10
123 |
124 | ### Fixed
125 | - Fixed issue where group middleware was being dublicated on internal routes.
126 |
127 | ## [4.1.0] 2018-09
128 |
129 | ### Changed
130 | - JSON strategy now allows array and object returns and builds JSON response. (Thanks @willemwollebrants)
131 |
132 | ### Fixed
133 | - Fixed issue where setting strategy on specific routes had no effect. (Thanks @aag)
134 |
135 | ## [4.0.1] 2018-08
136 |
137 | ### Fixed
138 | - Fixed a bug where content-type header would not be added to response in Json Strategy.
139 |
140 | ## [4.0.0] 2018-08
141 |
142 | ### Changed
143 | - Increased minimum PHP version to 7.1.0
144 | - Now implements PSR-15 middleware and request handlers.
145 | - No longer enforces use of container, one can be used optionally.
146 | - Strategies now return PSR-15 middleare as handlers.
147 | - Increased types of proxy callables that can be used as controllers.
148 | - General housekeeping and refactoring for continued improvement.
149 |
150 | ### Fixed
151 | - Group level strategies now handle exceptions if a route is not matched but the request falls within the group.
152 |
153 | ## [3.1.0] 2018-07
154 |
155 | ### Fixed
156 | - Ensure JsonStrategy handles all exceptions by default.
157 | - Handle multiline exception messages.
158 |
159 | ### Added
160 | - Add port condition to routes.
161 |
162 | ## 3.0.4 2017-03
163 |
164 | ### Fixed
165 | - Middleware execution order.
166 |
167 | ## 3.0.0 2017-03
168 |
169 | ## Added
170 | - Middleware functionality for PSR-7 compatible callables, globally to route collection or individually per route/group.
171 | - Allow setting of strategy for a route group.
172 | - Add UUID as default pattern matcher.
173 |
174 | ## Changed
175 | - Now depend directly on PSR-11 implementation.
176 | - Simplified default strategies to just `Application` and `Json`.
177 | - Have strategies return a middleware to add to the stack.
178 | - Have strategies handle decoration of exceptions.
179 |
180 | ## 2.0.2 - 2018-07
181 |
182 | ### Fixed
183 | - Have JsonStrategy handle all exceptions by default.
184 |
185 | ## 2.0.0 - 2016-02
186 |
187 | ### Added
188 | - All routing and dispatching now built around PSR-7.
189 | - Can now group routes with prefix and match conditions.
190 | - Routes now stored against a specific `Route` object that describes the route.
191 | - New `dispatch` method on `RouteCollection` that is a compliant PSR-7 middleware.
192 | - Additional route matching conditions for scheme and host.
193 |
194 | ### Changed
195 | - API rewrite to simplify.
196 | - API naming improvements.
197 | - Strategies now less opinionated about return from controller.
198 |
199 | ## [1.2.0] - 2015-08
200 |
201 | ### Added
202 | - Can now use any callable as a controller.
203 | - Request object is now built by the strategy when one is not available from the container.
204 |
205 | ### Fixed
206 | - General tidying and removal of unused code.
207 | - URI variables now correctly passed to controller in `MethodArgumentStrategy`.
208 |
209 | ## [1.1.0] - 2015-02
210 |
211 | ### Added
212 | - Added `addPatternMatcher` method to allow custom regex shortcuts within wildcard routes.
213 | - Refactored logic around matching routes.
214 |
215 | ## [1.0.1] - 2015-01
216 |
217 | ### Fixed
218 | - Added import statements for all used objects.
219 | - Fixed dockblock annotations.
220 | - PSR-2 standards improvements within tests.
221 |
222 | ## 1.0.0 - 2015-01
223 |
224 | ### Added
225 | - Migrated from [Orno\Route](https://github.com/orno/route).
226 |
--------------------------------------------------------------------------------
/src/Router.php:
--------------------------------------------------------------------------------
1 | '{$1:[0-9]+}',
41 | '/{(.+?):word}/' => '{$1:[a-zA-Z]+}',
42 | '/{(.+?):alphanum_dash}/' => '{$1:[a-zA-Z0-9-_]+}',
43 | '/{(.+?):slug}/' => '{$1:[a-z0-9-]+}',
44 | '/{(.+?):uuid}/' => '{$1:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}+}'
45 | ];
46 |
47 | /**
48 | * @var Route[]
49 | */
50 | protected array $routes = [];
51 |
52 | protected bool $routesPrepared = false;
53 |
54 | protected array $routesData = [];
55 |
56 | public function __construct(protected ?RouteCollector $routeCollector = null)
57 | {
58 | $this->routeCollector = $this->routeCollector ?? new RouteCollector(
59 | new RouteParser\Std(),
60 | new DataGenerator\GroupCountBased()
61 | );
62 | }
63 |
64 | public function addPatternMatcher(string $alias, string $regex): self
65 | {
66 | $pattern = '/{(.+?):' . $alias . '}/';
67 | $regex = '{$1:' . $regex . '}';
68 | $this->patternMatchers[$pattern] = $regex;
69 | return $this;
70 | }
71 |
72 | public function group(string $prefix, callable $group): RouteGroup
73 | {
74 | $group = new RouteGroup($prefix, $group, $this);
75 | $this->groups[] = $group;
76 | return $group;
77 | }
78 |
79 | public function dispatch(ServerRequestInterface $request): ResponseInterface
80 | {
81 | if (false === $this->routesPrepared) {
82 | $this->prepareRoutes($request);
83 | }
84 |
85 | /** @var Dispatcher $dispatcher */
86 | $dispatcher = (new Dispatcher($this->routesData))->setStrategy($this->getStrategy());
87 |
88 | foreach ($this->getMiddlewareStack() as $middleware) {
89 | if (is_string($middleware)) {
90 | $dispatcher->lazyMiddleware($middleware);
91 | continue;
92 | }
93 |
94 | $dispatcher->middleware($middleware);
95 | }
96 |
97 | return $dispatcher->dispatchRequest($request);
98 | }
99 |
100 | public function getNamedRoute(string $name): Route
101 | {
102 | if (!$this->routesPrepared) {
103 | $this->collectGroupRoutes();
104 | }
105 |
106 | $this->buildNameIndex();
107 |
108 | if (isset($this->namedRoutes[$name])) {
109 | return $this->namedRoutes[$name];
110 | }
111 |
112 | throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name));
113 | }
114 |
115 | public function handle(ServerRequestInterface $request): ResponseInterface
116 | {
117 | return $this->dispatch($request);
118 | }
119 |
120 | public function map(
121 | string|array $method,
122 | string $path,
123 | callable|array|string|RequestHandlerInterface $handler
124 | ): Route {
125 | $path = sprintf('/%s', ltrim($path, '/'));
126 | $route = new Route($method, $path, $handler);
127 |
128 | $this->routes[] = $route;
129 |
130 | return $route;
131 | }
132 |
133 | public function prepareRoutes(ServerRequestInterface $request): void
134 | {
135 | if ($this->getStrategy() === null) {
136 | $this->setStrategy(new ApplicationStrategy());
137 | }
138 |
139 | $this->processGroups($request);
140 | $this->buildNameIndex();
141 |
142 | $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));
143 | $options = [];
144 |
145 | /** @var Route $route */
146 | foreach ($routes as $route) {
147 | // this allows for the same route to be mapped across different routes/hosts etc
148 | if (false === $this->isExtraConditionMatch($route, $request)) {
149 | continue;
150 | }
151 |
152 | if ($route->getStrategy() === null) {
153 | $route->setStrategy($this->getStrategy());
154 | }
155 |
156 | $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
157 |
158 | // global strategy must be an OPTIONS handler to automatically generate OPTIONS route
159 | if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
160 | continue;
161 | }
162 |
163 | // need a messy but useful identifier to determine what methods to respond with on OPTIONS
164 | $identifier = $route->getScheme() . static::IDENTIFIER_SEPARATOR . $route->getHost()
165 | . static::IDENTIFIER_SEPARATOR . $route->getPort() . static::IDENTIFIER_SEPARATOR . $route->getPath();
166 |
167 | // if there is a defined OPTIONS route, do not generate one
168 | if ('OPTIONS' === $route->getMethod()) {
169 | unset($options[$identifier]);
170 | continue;
171 | }
172 |
173 | if (!isset($options[$identifier])) {
174 | $options[$identifier] = [];
175 | }
176 |
177 | $options[$identifier][] = $route->getMethod();
178 | }
179 |
180 | $this->buildOptionsRoutes($options);
181 |
182 | $this->routesPrepared = true;
183 | $this->routesData = $this->routeCollector->getData();
184 | }
185 |
186 | protected function buildNameIndex(): void
187 | {
188 | foreach ($this->routes as $key => $route) {
189 | if ($route->getName() !== null) {
190 | unset($this->routes[$key]);
191 | $this->namedRoutes[$route->getName()] = $route;
192 | }
193 | }
194 | }
195 |
196 | protected function buildOptionsRoutes(array $options): void
197 | {
198 | if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
199 | return;
200 | }
201 |
202 | /** @var OptionsHandlerInterface $strategy */
203 | $strategy = $this->getStrategy();
204 |
205 | foreach ($options as $identifier => $methods) {
206 | [$scheme, $host, $port, $path] = explode(static::IDENTIFIER_SEPARATOR, $identifier);
207 | $route = new Route('OPTIONS', $path, $strategy->getOptionsCallable($methods));
208 |
209 | if (!empty($scheme)) {
210 | $route->setScheme($scheme);
211 | }
212 |
213 | if (!empty($host)) {
214 | $route->setHost($host);
215 | }
216 |
217 | if (!empty($port)) {
218 | $route->setPort($port);
219 | }
220 |
221 | $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
222 | }
223 | }
224 |
225 | protected function collectGroupRoutes(): void
226 | {
227 | foreach ($this->groups as $group) {
228 | $group();
229 | }
230 | }
231 |
232 | protected function processGroups(ServerRequestInterface $request): void
233 | {
234 | $activePath = $request->getUri()->getPath();
235 |
236 | foreach ($this->groups as $key => $group) {
237 | // we want to determine if we are technically in a group even if the
238 | // route is not matched so exceptions are handled correctly
239 | if (
240 | $group->getStrategy() !== null
241 | && strncmp($activePath, $group->getPrefix(), strlen($group->getPrefix())) === 0
242 | ) {
243 | $this->setStrategy($group->getStrategy());
244 | }
245 |
246 | unset($this->groups[$key]);
247 | $group();
248 | }
249 | }
250 |
251 | protected function parseRoutePath(string $path): string
252 | {
253 | return preg_replace(array_keys($this->patternMatchers), array_values($this->patternMatchers), $path);
254 | }
255 | }
256 |
--------------------------------------------------------------------------------