├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Collector ├── LinkCollector.php └── RouterCollector.php ├── Exceptions ├── CannotGenerateLinkException.php ├── ChariotException.php ├── HttpException.php ├── InvalidArgumentException.php ├── LogicException.php └── PatternException.php ├── ExportableTrait.php ├── HttpMethods.php ├── InternalRoute.php ├── InternalRouteInterface.php ├── Link.php ├── LinkInterface.php ├── LinkParamsTrait.php ├── ParamDecorators ├── Context.php ├── ContextInterface.php └── ParamDecoratorInterface.php ├── Pattern ├── PatternInterface.php ├── PatternLink.php ├── PatternRoute.php ├── PatternRouteNode.php ├── PatternRouter.php ├── Patterns.php ├── PatternsInterface.php ├── RegexTester.php ├── SourceCodeExporter.php └── StdPatterns │ ├── AbstractPattern.php │ ├── DatePattern.php │ ├── FloatPattern.php │ ├── IntPattern.php │ ├── Ip4Pattern.php │ ├── ListPattern.php │ ├── RegexPattern.php │ ├── UnsignedFloatPattern.php │ └── UnsignedIntPattern.php ├── Reflections └── Objects.php └── RouterInterface.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### [0.5.0] - 2018-03-28 4 | 5 | * Changed - white space between first and second parameter is not required, 6 | if second parameter begins with `:`, e.g. `{{id:uint}}` 7 | * Replaced `: class` with `: self` wherever it was possible 8 | 9 | ### [0.4.0] - 2018-03-23 10 | 11 | * Fixed - `Awesomite\Chariot\Pattern\StdPatterns\ListPattern` should not allow for bool 12 | * Removed unused parameters from `Awesomite\Chariot\Exceptions\CannotGenerateLinkException::__construct` 13 | and `Awesomite\Chariot\Exceptions\HttpException::__construct` 14 | * Added `Awesomite\Chariot\ParamDecorators\ParamDecoratorInterface`, 15 | see examples: 16 | * `examples/param-decorator.php` 17 | * `examples/param-decorator2.php` 18 | * `examples/param-decorator-item.php` 19 | * Changed - cannot add pattern after unserialize `Awesomite\Chariot\Pattern\Patterns` 20 | * Changed - cannot add route after restoring router from cache 21 | ```php 22 | exportToExecutable() . ';'); 31 | $router->get('/', 'home'); 32 | ``` 33 | * Added restriction - each route for the same `handler` must contain the same list of parameters, e.g.: 34 | ```php 35 | get('/article-{{ id :int }}-{{ name }}', 'showArticle'); 42 | // hidden parameter 'name' = null is required 43 | $router->get('/article-{{ id :int }}', 'showArticle', ['name' => null]); 44 | ``` 45 | 46 | ### [0.3.1] - 2017-09-17 47 | 48 | * Fixed - `Awesomite\Chariot\Pattern\PatternRouter` did not work properly when pattern was prefixed by regular expression, 49 | e.g. `{{ subdomain }}.local/` 50 | 51 | ### [0.3.0] - 2017-09-13 52 | 53 | * Changed - method `Awesomite\Chariot\Pattern\Patterns::addPattern()` 54 | throws `Awesomite\Chariot\Exceptions\InvalidArgumentException` 55 | instead of `Awesomite\Chariot\Exceptions\LogicException` when argument `$name` is not prefixed by `:` 56 | * Changed - method `Awesomite\Chariot\Pattern\Patterns::addPattern()` 57 | accepts stringable object (with method `__toString`) as argument `$pattern` 58 | * Changed - everything outside `{{` double brackets `}}` is transformed by `preg_quote()` function. 59 | * Constant `Awesomite\Chariot\Pattern\Patterns::DELIMITER` instead of hardcoded value `#` 60 | 61 | ### [0.2.1] - 2017-08-27 62 | 63 | * Fixed regex `Awesomite\Chariot\Pattern\Patterns::REGEX_FLOAT` 64 | * Fixed regex `Awesomite\Chariot\Pattern\Patterns::REGEX_UFLOAT` 65 | * Fixed constant `Awesomite\Chariot\Pattern\Patterns::STANDARD_PATTERNS` (invalid value for `:ufloat`) 66 | 67 | ### [0.2.0] - 2017-08-27 68 | 69 | * Added `Awesomite\Chariot\Pattern\PatternInterface` - possibility to conversions url params, e.g. date in format `YYYY-mm-dd` to `DateTime` object 70 | * Added [behat] tests 71 | * Force pattern names prefixed by ":" 72 | * Changed `Awesomite\Chariot\Pattern\Patterns::createDefault()`, result is set of patterns: 73 | 74 | | name | action | class/regex | 75 | |-----------|-----------------|------------------------| 76 | | :int | changed | [IntPattern] | 77 | | :uint | changed | [UnsignedIntPattern] | 78 | | :float | added | [FloatPattern] | 79 | | :ufloat | added | [UnsignedFloatPattern] | 80 | | :date | added | [DatePattern] | 81 | | :list | added | [ListPattern] | 82 | | :ip4 | added | [Ip4Pattern] | 83 | | :alphanum | same as earlier | `[a-zA-Z0-9]+` | 84 | 85 | ### [0.1.0] - 2017-07-20 86 | 87 | * Initial public release 88 | 89 | [0.5.0]: https://github.com/awesomite/chariot/compare/v0.4.0...v0.5.0 90 | [0.4.0]: https://github.com/awesomite/chariot/compare/v0.3.1...v0.4.0 91 | [0.3.1]: https://github.com/awesomite/chariot/compare/v0.3.0...v0.3.1 92 | [0.3.0]: https://github.com/awesomite/chariot/compare/v0.2.1...v0.3.0 93 | [0.2.1]: https://github.com/awesomite/chariot/compare/v0.2.0...v0.2.1 94 | [0.2.0]: https://github.com/awesomite/chariot/compare/v0.1.0...v0.2.0 95 | [0.1.0]: https://github.com/awesomite/chariot/tree/v0.1.0 96 | [behat]: http://behat.org 97 | 98 | [IntPattern]: src/Pattern/StdPatterns/IntPattern.php 99 | [UnsignedIntPattern]: src/Pattern/StdPatterns/UnsignedIntPattern.php 100 | [FloatPattern]: src/Pattern/StdPatterns/FloatPattern.php 101 | [UnsignedFloatPattern]: src/Pattern/StdPatterns/UnsignedFloatPattern.php 102 | [DatePattern]: src/Pattern/StdPatterns/DatePattern.php 103 | [ListPattern]: src/Pattern/StdPatterns/ListPattern.php 104 | [Ip4Pattern]: src/Pattern/StdPatterns/Ip4Pattern.php 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 - 2018 Bartłomiej Krukowski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chariot 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ca2c76b33b5042d49658105bc5b63075)](https://www.codacy.com/app/awesomite/chariot?utm_source=github.com&utm_medium=referral&utm_content=awesomite/chariot&utm_campaign=Badge_Grade) 4 | [![Build Status](https://travis-ci.org/awesomite/chariot.svg?branch=master)](https://travis-ci.org/awesomite/chariot) 5 | [![Coverage Status](https://coveralls.io/repos/github/awesomite/chariot/badge.svg?branch=master)](https://coveralls.io/github/awesomite/chariot?branch=master) 6 | 7 | Just another routing library. Makes human-friendly URLs and programmer-friendly code. 8 | Uses trees for the best performance. 9 | 10 | [github.com/awesomite/chariot](https://github.com/awesomite/chariot) 11 | 12 | ## Why? 13 | 14 | To simplify creating human-friendly URLs. 15 | 16 | ```php 17 | linkTo('showArticle')->withParam('id', 5); 21 | ``` 22 | 23 | ## Table of contents 24 | * [How does it work?](#how-does-it-work) 25 | * [Patterns](#patterns) 26 | * [Parameters](#parameters) 27 | * [Examples](#examples) 28 | * [Routing](#routing) 29 | * [Generating links](#generating-links) 30 | * [Hidden parameters](#hidden-parameters) 31 | * [Caching](#caching) 32 | * [Defining custom patterns](#defining-custom-patterns) 33 | * [Validation](#validation) 34 | * [Default parameters](#default-parameters) 35 | * [Transforming parameters](#transforming-parameters) 36 | * [Providers / Decorators](#providers--decorators) 37 | * [Default patterns](#default-patterns) 38 | * [More examples](#more-examples) 39 | * [License](#license) 40 | * [Versioning](#versioning) 41 | 42 | ## How does it work? 43 | 44 | ### Patterns 45 | 46 | Patterns are designed to maximally simplify creating routing in your application. 47 | Patterns can have parameters packed in `{{` double curly brackets `}}`. 48 | 49 | #### Parameters 50 | 51 | Parameters contains three values separated by one or more white characters. 52 | Second and third values are optional. 53 | First value is just a name. 54 | Second value is a regular expression or name of registered regular expression, 55 | default value is equal to `[^/]+`. 56 | Third value contains default value of parameter (used for generating links). 57 | 58 | #### Examples 59 | 60 | I believe that the best documentation are examples from the real world. The following patterns should help you to understand how does it work. 61 | 62 | * `/page/{{ page :uint }}` 63 | * `/page/{{page:uint}}` (white space between first and second parameter is not required, if second parameter begins with `:`) 64 | * `/page/{{page \d+ 1}}` 65 | * `/categories/{{ name [a-zA-Z0-9-]+ }}` 66 | * `/categories/{{ categoryName }}/item-{{ itemId :uint }}` 67 | 68 | ### Routing 69 | 70 | ```php 71 | addRoute(HttpMethods::METHOD_GET, '/', 'home'); 79 | 80 | $method = 'GET'; 81 | $path = '/'; 82 | 83 | try { 84 | $route = $router->match($method, $path); 85 | $handler = $route->getHandler(); 86 | echo $handler, "\n"; 87 | } catch (HttpException $exception) { 88 | echo $exception->getMessage(), "\n"; 89 | 90 | // code can be equal to 404 or 405 91 | if ($exception->getCode() === HttpException::HTTP_METHOD_NOT_ALLOWED) { 92 | echo 'Allow: ', implode(', ', $router->getAllowedMethods($path)), "\n"; 93 | } 94 | } 95 | ``` 96 | 97 | ### Generating links 98 | 99 | ```php 100 | addRoute(HttpMethods::METHOD_GET, '/category-{{ category :int }}', 'showCategory'); 107 | 108 | echo $router->linkTo('showCategory')->withParam('category', 5), "\n"; 109 | /* 110 | * Output: 111 | * /category-5 112 | */ 113 | ``` 114 | 115 | ### Hidden parameters 116 | 117 | Hidden parameters are a very useful mechanism. 118 | Using them allows you to completely change routing in your application without changing code in a lot of places. 119 | Let's look at the following scenario: 120 | 121 | 1. You have a category page in your website. 122 | 2. Path pattern to category page is equal to `/category-{{ id :uint }}`. 123 | 3. Link to category page is generated in many places in your code. Let's say **100**. 124 | 4. You want to change approach. Category's id in a link is not expected anymore. You want to have human-friendly links, e.g. `/books` instead of `/category-15`. 125 | 5. Using *old school* way of generating links forced you to rewrite code in **100** places. You have to spend time to rewriting code. The risk of error is high. 126 | 127 | Instead of monotonous rewriting code you can change only one thing in routing. 128 | This approuch helps you to save your time and protects your code from bugs. 129 | The following pieces of code should help you understand how to rewrite routing. 130 | 131 | **Old code** 132 | ```php 133 | $router->get('/category-{{ id :uint }}', 'showCategory'); 134 | ``` 135 | 136 | **New code** 137 | ```php 138 | $router 139 | ->get('/fantasy', 'showCategory', ['id' => 1]) 140 | ->get('/comedy', 'showCategory', ['id' => 2]); 141 | ``` 142 | 143 | **Note:** 144 | In this case small number of categories (let's say 100) will not cause performance issue. 145 | But keep in your mind - big number of routes assigned to one handler can slow down generating links. 146 | I encourage you to execute performance tests on your machine. 147 | Exemplary test is attached to this repository, execute the following commands to perform it: 148 | 149 | ```bash 150 | git clone --depth 1 git@github.com:awesomite/chariot.git 151 | cd chariot 152 | composer update 153 | php speedtest/console.php test-links 154 | ``` 155 | 156 | **Bigger example** 157 | ```php 158 | get('/show-first-page', 'showPage', [ 164 | 'page' => 1, 165 | ]); 166 | $router->get('/page-{{ page :uint }}', 'showPage'); 167 | 168 | $route = $router->match('GET', '/show-first-page'); 169 | echo $route->getHandler(), "\n"; 170 | var_dump($route->getParams()); 171 | /* 172 | * Output: 173 | * showPage 174 | * array(1) { 175 | * 'page' => 176 | * int(1) 177 | * } 178 | */ 179 | 180 | echo $router->linkTo('showPage')->withParam('page', 1), "\n"; // /show-first-page 181 | echo $router->linkTo('showPage')->withParam('page', 2), "\n"; // /page-2 182 | /* 183 | * Output: 184 | * /show-first-page 185 | * /page-2 186 | */ 187 | ``` 188 | 189 | ### Caching 190 | 191 | ```php 192 | cacheFile = $cacheFile; 203 | } 204 | 205 | public function rebuildRouter() 206 | { 207 | $router = $this->createRouter(); 208 | file_put_contents($this->cacheFile, 'exportToExecutable() . ';'); 209 | } 210 | 211 | public function getRouter(): PatternRouter 212 | { 213 | return require $this->cacheFile; 214 | } 215 | 216 | private function createRouter(): PatternRouter 217 | { 218 | return PatternRouter::createDefault() 219 | ->get('/', 'showHome') 220 | ->get('/news', 'showNews', ['page' => 1]) 221 | ->get('/news/{{ page :int }}', 'showNews'); 222 | } 223 | } 224 | 225 | $factory = new RouterFactory(__DIR__ . DIRECTORY_SEPARATOR . 'router.cache'); 226 | // Executing this function once is enough, e.g. during warmup 227 | $factory->rebuildRouter(); 228 | $router = $factory->getRouter(); 229 | // decorators are not cacheable, you must add them each time 230 | // $router->addParamDecorator(new MyParamDecorator()); 231 | ``` 232 | 233 | ### Defining custom patterns 234 | 235 | ```php 236 | getPatterns() 249 | ->addPattern(':date', '[0-9]{4}-[0-9]{2}-[0-9]{2}') 250 | ->addEnumPattern(':category', $categories); 251 | 252 | $router->get('/day-{{ date :date }}', 'showDay'); 253 | $route = $router->match('GET', '/day-2017-01-01'); 254 | echo $route->getParams()['date'], "\n"; // 2017-01-01 255 | 256 | $router->get('/category-{{ category :category }}', 'showCategory'); 257 | $route = $router->match('GET', '/category-comedy'); 258 | echo $route->getParams()['category'], "\n"; // comedy 259 | ``` 260 | 261 | ### Validation 262 | 263 | **Chariot** checks correctness of values incoming (routing) and outcoming (generating links). 264 | 265 | **Note:** 266 | Method `PatternRouter->linkTo()` returns instance of `LinkInterface`. 267 | [Read description](src/LinkInterface.php) to understand difference between `toString()` and `__toString()` methods. 268 | 269 | ```php 270 | get('/category-{{ categoryId :int }}', 'showCategory'); 278 | 279 | /* 280 | * The following code displays "Error 404" 281 | */ 282 | try { 283 | $route = $router->match('GET', '/category-books'); 284 | echo "Handler:\n", $route->getHandler(), "\n"; 285 | echo "Params:\n"; 286 | var_dump($route->getParams()); 287 | } catch (HttpException $exception) { 288 | echo 'Error ', $exception->getCode(), "\n"; 289 | } 290 | 291 | /* 292 | * The following code displays "Cannot generate link" 293 | */ 294 | try { 295 | echo $router->linkTo('showCategory')->withParam('categoryId', 'books')->toString(), "\n"; 296 | } catch (CannotGenerateLinkException $exception) { 297 | echo "Cannot generate link\n"; 298 | } 299 | ``` 300 | 301 | ### Default parameters 302 | 303 | ```php 304 | get('/articles/{{ page :uint 1 }}', 'articles'); 310 | echo $router->linkTo('articles'), "\n"; 311 | echo $router->linkTo('articles')->withParam('page', 2), "\n"; 312 | 313 | /* 314 | * Output: 315 | * /articles/1 316 | * /articles/2 317 | */ 318 | ``` 319 | 320 | ### Transforming parameters 321 | 322 | Router can transform parameter extracted from URL (and parameter passed to URL). 323 | Passed object to method addPattern() must implements interface PatternInterface. 324 | @See [PatternInterface](src/Pattern/PatternInterface.php). 325 | 326 | ```php 327 | getPatterns()->addPattern(':date', new DatePattern()); 339 | $router->get('/day/{{ day :date }}', 'showDay'); 340 | echo $router->linkTo('showDay')->withParam('day', new \DateTime('2017-07-07')), "\n"; 341 | 342 | /* 343 | * Output: 344 | * /day/2017-07-07 345 | */ 346 | ``` 347 | 348 | ### Providers / Decorators 349 | 350 | Let's imagine the following scenario: 351 | 352 | 1) You have prepared big web-application. 353 | 2) Application is using the following pattern to display items `/items/{{ id :int }}`. 354 | 3) Next goal is add title of item to url (`/items/{{ id :int }}-{{ title }}`). 355 | 356 | You can just change URL pattern and add code `withParam('title', $title)` wherever application generates url to item. 357 | Chariot allows resolve such issues better and faster. The following code explains how to use providers. 358 | See: 359 | * [ContextInterface](src/ParamDecorators/ContextInterface.php) 360 | * [ParamDecoratorInterface](src/ParamDecorators/ParamDecoratorInterface.php) 361 | 362 | ```php 363 | mapping = $mapping; 376 | } 377 | 378 | public function decorate(ContextInterface $context) 379 | { 380 | if ('showItem' !== $context->getHandler()) { 381 | return; 382 | } 383 | 384 | $id = $context->getParams()['id'] ?? null; 385 | $title = $this->mapping[$id] ?? null; 386 | 387 | if (null !== $title) { 388 | $context->setParam('title', $title); 389 | } 390 | } 391 | } 392 | 393 | $titleMapping = [ 394 | 1 => 'my-first-item', 395 | 2 => 'my-second-item', 396 | 3 => 'my-third-item', 397 | ]; 398 | $router = PatternRouter::createDefault(); 399 | $router->get('/items/{{ id :int }}-{{ title }}', 'showItem'); 400 | 401 | /* 402 | * Error, because title is not defined 403 | */ 404 | echo 'Without provider: '; 405 | echo $router->linkTo('showItem')->withParam('id', 1), PHP_EOL; 406 | 407 | /* 408 | * Valid URL, because title will be provided automatically 409 | */ 410 | $router->addParamDecorator(new TitleProvider($titleMapping)); 411 | echo 'With provider: '; 412 | echo $router->linkTo('showItem')->withParam('id', 1), PHP_EOL; 413 | 414 | /* 415 | * Output: 416 | * 417 | * Without provider: __ERROR_CANNOT_GENERATE_LINK 418 | * With provider: /items/1-my-first-item 419 | */ 420 | ``` 421 | 422 | ### Default patterns 423 | 424 | Method `Awesomite\Chariot\Pattern\Patterns::createDefault()` 425 | returns instance of `Awesomite\Chariot\Pattern\Patterns` 426 | with set of standard patterns: 427 | 428 | | name | input | output | class/regex | 429 | |-----------|------------------|---------------------------------------|------------------------| 430 | | :int | `-5` | `(int) -5` | [IntPattern] | 431 | | :uint | `5` | `(int) 5` | [UnsignedIntPattern] | 432 | | :float | `-5.05` | `(float) -5.05` | [FloatPattern] | 433 | | :ufloat | `5.05` | `(float) 5.05` | [UnsignedFloatPattern] | 434 | | :date | `2017-01-01` | `new DateTimeImmutable("2017-01-01")` | [DatePattern] | 435 | | :list | `red,green,blue` | `(array) ["red", "green", "blue"]` | [ListPattern] | 436 | | :ip4 | `8.8.8.8` | `(string) "8.8.8.8"` | [Ip4Pattern] | 437 | | :alphanum | `nickname2000` | `(string) "nickname2000"` | `[a-zA-Z0-9]+` | 438 | 439 | ## More examples 440 | 441 | * [Own micro framework](examples/micro-framework.php) 442 | * [Months](examples/months.php) 443 | * Param decorators: [example1](examples/param-decorator.php), [example2](examples/param-decorator2.php), 444 | [example3](examples/param-decorator-item.php) 445 | * [Symfony integration](examples/symfony.php) 446 | * [Transforming parameters](examples/transform-params.php) 447 | 448 | ## License 449 | 450 | MIT - [read license](LICENSE) 451 | 452 | ## Versioning 453 | 454 | The version numbers follow the [Semantic Versioning 2.0.0](http://semver.org/) scheme. 455 | 456 | [IntPattern]: src/Pattern/StdPatterns/IntPattern.php 457 | [UnsignedIntPattern]: src/Pattern/StdPatterns/UnsignedIntPattern.php 458 | [FloatPattern]: src/Pattern/StdPatterns/FloatPattern.php 459 | [UnsignedFloatPattern]: src/Pattern/StdPatterns/UnsignedFloatPattern.php 460 | [DatePattern]: src/Pattern/StdPatterns/DatePattern.php 461 | [ListPattern]: src/Pattern/StdPatterns/ListPattern.php 462 | [Ip4Pattern]: src/Pattern/StdPatterns/Ip4Pattern.php 463 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awesomite/chariot", 3 | "description": "Just another routing library", 4 | "keywords": ["routing", "router"], 5 | "type": "library", 6 | "require": { 7 | "php": "^7.0", 8 | "ext-pcre": "*" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^6.5.7 || ^7.0.2", 12 | "symfony/console": "^3.4.6", 13 | "symfony/http-foundation": "^3.4.6", 14 | "symfony/finder": "^3.4.6", 15 | "behat/behat": "^3.4.3", 16 | "composer/xdebug-handler": "1.0.0" 17 | }, 18 | "minimum-stability": "RC", 19 | "prefer-stable": true, 20 | "license": "MIT", 21 | "authors": [ 22 | { 23 | "name": "Bartłomiej Krukowski", 24 | "email": "bartlomiej@krukowski.me" 25 | } 26 | ], 27 | "autoload": { 28 | "psr-4": { 29 | "Awesomite\\Chariot\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "": "features/src/", 35 | "Awesomite\\Chariot\\": "tests/", 36 | "Awesomite\\Chariot\\Speedtest\\": "speedtest/src/" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Collector/LinkCollector.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Collector; 11 | 12 | use Awesomite\Chariot\Exceptions\CannotGenerateLinkException; 13 | use Awesomite\Chariot\LinkInterface; 14 | use Awesomite\Chariot\LinkParamsTrait; 15 | 16 | /** 17 | * @internal 18 | */ 19 | class LinkCollector implements LinkInterface 20 | { 21 | use LinkParamsTrait; 22 | 23 | /** 24 | * @var callable[] 25 | */ 26 | private $getters; 27 | 28 | private $handler; 29 | 30 | /** 31 | * @param string $handler 32 | * @param callable[] $getters 33 | */ 34 | public function __construct(string $handler, array $getters) 35 | { 36 | $this->handler = $handler; 37 | $this->getters = $getters; 38 | } 39 | 40 | public function toString(): string 41 | { 42 | $result = (string) $this; 43 | if ($result === static::ERROR_CANNOT_GENERATE_LINK) { 44 | throw new CannotGenerateLinkException($this->handler, $this->params); 45 | } 46 | 47 | return $result; 48 | } 49 | 50 | public function __toString(): string 51 | { 52 | foreach ($this->getters as $getter) { 53 | /** @var LinkInterface $link */ 54 | $link = $getter(); 55 | $link->withParams($this->params); 56 | $link->withPrefix($this->prefix); 57 | $result = (string) $link; 58 | if ($result !== static::ERROR_CANNOT_GENERATE_LINK) { 59 | return $result; 60 | } 61 | } 62 | 63 | return static::ERROR_CANNOT_GENERATE_LINK; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Collector/RouterCollector.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Collector; 11 | 12 | use Awesomite\Chariot\Exceptions\HttpException; 13 | use Awesomite\Chariot\HttpMethods; 14 | use Awesomite\Chariot\InternalRouteInterface; 15 | use Awesomite\Chariot\LinkInterface; 16 | use Awesomite\Chariot\RouterInterface; 17 | 18 | class RouterCollector implements RouterInterface 19 | { 20 | /** 21 | * @var RouterInterface[] 22 | */ 23 | private $routers = []; 24 | 25 | public function addRouter(RouterInterface $router): self 26 | { 27 | $this->routers[] = $router; 28 | 29 | return $this; 30 | } 31 | 32 | public function match(string $method, string $path): InternalRouteInterface 33 | { 34 | $errorCode = HttpException::HTTP_NOT_FOUND; 35 | foreach ($this->routers as $router) { 36 | try { 37 | return $router->match($method, $path); 38 | } catch (HttpException $exception) { 39 | if (HttpException::HTTP_METHOD_NOT_ALLOWED === $exception->getCode()) { 40 | $errorCode = HttpException::HTTP_METHOD_NOT_ALLOWED; 41 | } 42 | } 43 | } 44 | 45 | throw new HttpException($method, $path, $errorCode); 46 | } 47 | 48 | public function getAllowedMethods(string $url): array 49 | { 50 | $result = []; 51 | foreach ($this->routers as $router) { 52 | $result = \array_merge($result, $router->getAllowedMethods($url)); 53 | } 54 | 55 | return \array_unique($result); 56 | } 57 | 58 | public function linkTo(string $handler, string $method = HttpMethods::METHOD_ANY): LinkInterface 59 | { 60 | $getters = []; 61 | foreach ($this->routers as $router) { 62 | $getters[] = function () use ($router, $handler, $method) { 63 | return $router->linkTo($handler, $method); 64 | }; 65 | } 66 | 67 | return new LinkCollector($handler, $getters); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Exceptions/CannotGenerateLinkException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Exceptions; 11 | 12 | class CannotGenerateLinkException extends LogicException 13 | { 14 | public function __construct(string $handler, array $params) 15 | { 16 | $message = "Cannot generate link for {$handler}"; 17 | if ($params) { 18 | $message .= '?' . \urldecode(\http_build_query($params)); 19 | } 20 | parent::__construct($message); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exceptions/ChariotException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Exceptions; 11 | 12 | interface ChariotException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exceptions/HttpException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Exceptions; 11 | 12 | class HttpException extends \Exception implements ChariotException 13 | { 14 | const HTTP_NOT_FOUND = 404; 15 | const HTTP_METHOD_NOT_ALLOWED = 405; 16 | 17 | private static $translations 18 | = [ 19 | self::HTTP_NOT_FOUND => '404 Not Found', 20 | self::HTTP_METHOD_NOT_ALLOWED => '405 Method Not Allowed', 21 | ]; 22 | 23 | public function __construct(string $method, string $path, int $code) 24 | { 25 | parent::__construct($this->translateCode($code) . ": {$method} {$path}", $code); 26 | } 27 | 28 | private function translateCode(int $code): string 29 | { 30 | if (isset(self::$translations[$code])) { 31 | return self::$translations[$code]; 32 | } 33 | 34 | throw new InvalidArgumentException("Code {$code} cannot be translated"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Exceptions; 11 | 12 | class InvalidArgumentException extends \InvalidArgumentException implements ChariotException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exceptions/LogicException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Exceptions; 11 | 12 | class LogicException extends \LogicException implements ChariotException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exceptions/PatternException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Exceptions; 11 | 12 | use Awesomite\Chariot\Pattern\PatternInterface; 13 | 14 | /** 15 | * May be thrown when: 16 | * - invalid parameter is passed to PatternInterface::fromUrl() 17 | * - invalid parameter is passed to PatternInterface::toUrl() 18 | * 19 | * @see PatternInterface::fromUrl() 20 | * @see PatternInterface::toUrl() 21 | */ 22 | class PatternException extends InvalidArgumentException 23 | { 24 | const CODE_TO_URL = 1; 25 | const CODE_FROM_URL = 2; 26 | } 27 | -------------------------------------------------------------------------------- /src/ExportableTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | /** 13 | * @internal 14 | */ 15 | trait ExportableTrait 16 | { 17 | /** 18 | * @internal 19 | * 20 | * @param $data 21 | * 22 | * @return object 23 | */ 24 | public static function __set_state($data) 25 | { 26 | $reflection = new \ReflectionClass(static::class); 27 | $result = $reflection->newInstanceWithoutConstructor(); 28 | foreach ($data as $key => $value) { 29 | $result->$key = $value; 30 | } 31 | 32 | return $result; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/HttpMethods.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | class HttpMethods 13 | { 14 | const METHOD_ANY = '*'; 15 | const METHOD_HEAD = 'HEAD'; 16 | const METHOD_GET = 'GET'; 17 | const METHOD_POST = 'POST'; 18 | const METHOD_PUT = 'PUT'; 19 | const METHOD_DELETE = 'DELETE'; 20 | const METHOD_PATCH = 'PATCH'; 21 | const METHOD_CONNECT = 'CONNECT'; 22 | const METHOD_OPTIONS = 'OPTIONS'; 23 | const METHOD_TRACE = 'TRACE'; 24 | 25 | const ALL_METHODS 26 | = [ 27 | self::METHOD_ANY, 28 | self::METHOD_HEAD, 29 | self::METHOD_GET, 30 | self::METHOD_POST, 31 | self::METHOD_PUT, 32 | self::METHOD_DELETE, 33 | self::METHOD_PATCH, 34 | self::METHOD_CONNECT, 35 | self::METHOD_OPTIONS, 36 | self::METHOD_TRACE, 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /src/InternalRoute.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | /** 13 | * @internal 14 | */ 15 | class InternalRoute implements InternalRouteInterface 16 | { 17 | private $handler; 18 | 19 | private $extraParams; 20 | 21 | public function __construct(string $handler, array $extraParams) 22 | { 23 | $this->handler = $handler; 24 | $this->extraParams = $extraParams; 25 | } 26 | 27 | public function getParams(): array 28 | { 29 | return $this->extraParams; 30 | } 31 | 32 | public function getHandler(): string 33 | { 34 | return $this->handler; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/InternalRouteInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | interface InternalRouteInterface 13 | { 14 | public function getHandler(): string; 15 | 16 | public function getParams(): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Link.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | /** 13 | * @internal 14 | */ 15 | class Link implements LinkInterface 16 | { 17 | use LinkParamsTrait; 18 | 19 | private $base; 20 | 21 | public function __construct(string $base) 22 | { 23 | $this->base = $base; 24 | } 25 | 26 | public function __toString(): string 27 | { 28 | return $this->toString(); 29 | } 30 | 31 | public function toString(): string 32 | { 33 | return $this->prefix . $this->base . 34 | ($this->params ? '?' . \urldecode(\http_build_query($this->normalizeParams($this->params))) : ''); 35 | } 36 | 37 | private function normalizeParams($params) 38 | { 39 | \array_walk_recursive($params, function (&$value) { 40 | if (\is_object($value)) { 41 | if (\method_exists($value, '__toString')) { 42 | $value = (string) $value; 43 | 44 | return; 45 | } 46 | 47 | if ($value instanceof \Traversable) { 48 | $value = $this->normalizeParams(\iterator_to_array($value)); 49 | 50 | return; 51 | } 52 | } 53 | }); 54 | 55 | return $params; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/LinkInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | use Awesomite\Chariot\Exceptions\CannotGenerateLinkException; 13 | 14 | interface LinkInterface 15 | { 16 | const ERROR_CANNOT_GENERATE_LINK = '__ERROR_CANNOT_GENERATE_LINK'; 17 | 18 | public function withParam(string $key, $value): self; 19 | 20 | public function withParams(array $params): self; 21 | 22 | /** 23 | * Works same as __toString() method with one exception: 24 | * throws exception in case of error. 25 | * 26 | * @return string 27 | * 28 | * @throws CannotGenerateLinkException 29 | */ 30 | public function toString(): string; 31 | 32 | /** 33 | * PHP does not allow to throw an exception from within __toString() method. 34 | * Because of this fact __toString() method returns LinkInterface::ERROR_CANNOT_GENERATE_LINK in case of error. 35 | * 36 | * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring 37 | * @see LinkInterface::ERROR_CANNOT_GENERATE_LINK 38 | * 39 | * @return string 40 | */ 41 | public function __toString(): string; 42 | 43 | public function withPrefix(string $prefix): self; 44 | } 45 | -------------------------------------------------------------------------------- /src/LinkParamsTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | /** 13 | * @internal 14 | */ 15 | trait LinkParamsTrait 16 | { 17 | private $params = []; 18 | 19 | private $prefix = ''; 20 | 21 | public function withPrefix(string $prefix): LinkInterface 22 | { 23 | $this->prefix = $prefix; 24 | 25 | return $this; 26 | } 27 | 28 | public function withParams(array $params): LinkInterface 29 | { 30 | foreach ($params as $key => $value) { 31 | $this->withParam($key, $value); 32 | } 33 | 34 | return $this; 35 | } 36 | 37 | public function withParam(string $key, $value): LinkInterface 38 | { 39 | $this->params[$key] = $value; 40 | 41 | return $this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ParamDecorators/Context.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\ParamDecorators; 11 | 12 | /** 13 | * @internal 14 | */ 15 | class Context implements ContextInterface 16 | { 17 | private $handler; 18 | 19 | private $method; 20 | 21 | private $params; 22 | 23 | private $requiredParams; 24 | 25 | public function __construct(string $handler, string $method, array $params, array $requiredParams) 26 | { 27 | $this->handler = $handler; 28 | $this->method = $method; 29 | $this->params = $params; 30 | $this->requiredParams = $requiredParams; 31 | } 32 | 33 | public function getHandler(): string 34 | { 35 | return $this->handler; 36 | } 37 | 38 | public function getMethod(): string 39 | { 40 | return $this->method; 41 | } 42 | 43 | public function getParams(): array 44 | { 45 | return $this->params; 46 | } 47 | 48 | public function setParam(string $key, $value): ContextInterface 49 | { 50 | $this->params[$key] = $value; 51 | 52 | return $this; 53 | } 54 | 55 | public function removeParam(string $key): ContextInterface 56 | { 57 | unset($this->params[$key]); 58 | 59 | return $this; 60 | } 61 | 62 | public function getRequiredParams(): array 63 | { 64 | return $this->requiredParams; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ParamDecorators/ContextInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\ParamDecorators; 11 | 12 | interface ContextInterface 13 | { 14 | /** 15 | * e.g. 'showHomepage' 16 | * 17 | * @return string 18 | */ 19 | public function getHandler(): string; 20 | 21 | /** 22 | * e.g. 'GET' 23 | * 24 | * @return string 25 | */ 26 | public function getMethod(): string; 27 | 28 | /** 29 | * e.g. ['id' => 5, 'name' => 'chariot'] 30 | * 31 | * @return array 32 | */ 33 | public function getParams(): array; 34 | 35 | public function setParam(string $key, $value): self; 36 | 37 | public function removeParam(string $key): self; 38 | 39 | /** 40 | * e.g. ['id', 'name'] 41 | * 42 | * @return array 43 | */ 44 | public function getRequiredParams(): array; 45 | } 46 | -------------------------------------------------------------------------------- /src/ParamDecorators/ParamDecoratorInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\ParamDecorators; 11 | 12 | interface ParamDecoratorInterface 13 | { 14 | public function decorate(ContextInterface $context); 15 | } 16 | -------------------------------------------------------------------------------- /src/Pattern/PatternInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\Exceptions\PatternException; 13 | 14 | interface PatternInterface extends \Serializable 15 | { 16 | /** 17 | * Returns regex without delimiters 18 | * 19 | * @return string 20 | */ 21 | public function getRegex(): string; 22 | 23 | /** 24 | * @param mixed $data 25 | * 26 | * @return string 27 | * 28 | * @throws PatternException 29 | */ 30 | public function toUrl($data): string; 31 | 32 | /** 33 | * Passed argument must be validated by regex earlier 34 | * 35 | * @param string $param 36 | * 37 | * @return mixed 38 | * 39 | * @throws PatternException 40 | * 41 | * @see PatternInterface::getRegex() 42 | */ 43 | public function fromUrl(string $param); 44 | } 45 | -------------------------------------------------------------------------------- /src/Pattern/PatternLink.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\Exceptions\CannotGenerateLinkException; 13 | use Awesomite\Chariot\LinkInterface; 14 | use Awesomite\Chariot\LinkParamsTrait; 15 | use Awesomite\Chariot\ParamDecorators\Context; 16 | use Awesomite\Chariot\ParamDecorators\ParamDecoratorInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | class PatternLink implements LinkInterface 22 | { 23 | use LinkParamsTrait; 24 | 25 | private $method; 26 | 27 | private $handler; 28 | 29 | /** 30 | * [[$patternRoute, $extraParams], ...] 31 | * 32 | * @var array 33 | */ 34 | private $routes; 35 | 36 | private $sorted = false; 37 | 38 | /** 39 | * @var \SplObjectStorage|ParamDecoratorInterface[] 40 | */ 41 | private $paramDecorators; 42 | 43 | private $required; 44 | 45 | public function __construct(string $method, string $handler, array $routes, \SplObjectStorage $paramDecorators, array $required) 46 | { 47 | $this->method = $method; 48 | $this->handler = $handler; 49 | $this->routes = $routes; 50 | $this->paramDecorators = $paramDecorators; 51 | $this->required = $required; 52 | } 53 | 54 | public function __toString(): string 55 | { 56 | try { 57 | return $this->toString(); 58 | } catch (CannotGenerateLinkException $exception) { 59 | return static::ERROR_CANNOT_GENERATE_LINK; 60 | } 61 | } 62 | 63 | public function toString(): string 64 | { 65 | $this->sortIfNeed(); 66 | $this->handleDecorators(); 67 | foreach ($this->routes as list($route, $extraParams)) { 68 | $currentParams = $this->params; 69 | /** @var PatternRoute $route */ 70 | /** @var array $extraParams */ 71 | foreach ($extraParams as $key => $value) { 72 | if (!\array_key_exists($key, $this->params) || $this->normalizeVar($this->params[$key]) != $value) { 73 | continue 2; 74 | } 75 | unset($currentParams[$key]); 76 | } 77 | $convertedParams = $route->matchParams($currentParams); 78 | if (\is_array($convertedParams)) { 79 | return $this->prefix . (string) $route->bindParams(\array_replace($currentParams, $convertedParams)); 80 | } 81 | } 82 | 83 | throw new CannotGenerateLinkException($this->handler, $this->params); 84 | } 85 | 86 | private function handleDecorators() 87 | { 88 | foreach ($this->paramDecorators as $decorator) { 89 | $context = new Context($this->handler, $this->method, $this->params, $this->required); 90 | $decorator->decorate($context); 91 | $this->params = $context->getParams(); 92 | } 93 | } 94 | 95 | private function normalizeVar($var) 96 | { 97 | if (\is_object($var)) { 98 | if ($var instanceof \Traversable) { 99 | return $this->normalizeVar(\iterator_to_array($var)); 100 | } 101 | 102 | if (\method_exists($var, '__toString')) { 103 | return (string) $var; 104 | } 105 | } 106 | 107 | if (\is_array($var)) { 108 | \array_walk_recursive($var, function ($element) { 109 | return $this->normalizeVar($element); 110 | }); 111 | 112 | return $var; 113 | } 114 | 115 | return $var; 116 | } 117 | 118 | private function sortIfNeed() 119 | { 120 | if ($this->sorted) { 121 | return; 122 | } 123 | 124 | \usort($this->routes, function ($left, $right) { 125 | return \count($right[1]) <=> \count($left[1]); 126 | }); 127 | $this->sorted = true; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Pattern/PatternRoute.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\Exceptions\InvalidArgumentException; 13 | use Awesomite\Chariot\Exceptions\LogicException; 14 | use Awesomite\Chariot\Exceptions\PatternException; 15 | use Awesomite\Chariot\ExportableTrait; 16 | use Awesomite\Chariot\Link; 17 | use Awesomite\Chariot\LinkInterface; 18 | 19 | /** 20 | * @internal 21 | */ 22 | class PatternRoute 23 | { 24 | use ExportableTrait; 25 | 26 | const PATTERN_VAR = '/{{.*?}?}}/'; 27 | 28 | /** 29 | * Human-friendly pattern, i.e. "/{{ controller }}" 30 | * 31 | * @var string 32 | */ 33 | private $pattern; 34 | 35 | /** 36 | * Regex, e.g. "#^/(?[^/]+)$#" 37 | * 38 | * @var string 39 | */ 40 | private $compiledPattern; 41 | 42 | /** 43 | * Pattern without types declaration, i.e. /controller/{{action}} 44 | * 45 | * @var string 46 | */ 47 | private $simplePattern; 48 | 49 | /** 50 | * [$name = [$default, $pattern, $patternName|null], ...] 51 | * e.g. ['id' => [null, '#^-?[0-9]+$#', ':int'], ...] 52 | * 53 | * @var array 54 | */ 55 | private $explodedParams; 56 | 57 | /** 58 | * @var PatternsInterface 59 | */ 60 | private $patterns; 61 | 62 | public function __construct(string $pattern, PatternsInterface $patterns) 63 | { 64 | $this->pattern = $pattern; 65 | $this->patterns = $patterns; 66 | $this->processPattern(); 67 | } 68 | 69 | public function getRequiredParams(): array 70 | { 71 | return \array_keys($this->explodedParams); 72 | } 73 | 74 | private function processPattern() 75 | { 76 | $simplePattern = $this->pattern; 77 | 78 | $toCompile = $this->pattern; 79 | $compiledParts = []; 80 | $explodedParams = []; 81 | 82 | $usedNames = []; 83 | 84 | foreach ($this->processTokens() as list($token, $name, $pattern, $default, $patternName)) { 85 | if (\in_array($name, $usedNames, true)) { 86 | throw new LogicException(\sprintf( 87 | 'Parameter %s has been redeclared (source: %s)', 88 | $name, 89 | $this->pattern 90 | )); 91 | } 92 | $usedNames[] = $name; 93 | 94 | // simple pattern /item-{{id}} 95 | $simplePattern = $this->replaceFirst($token, '{{' . $name . '}}', $simplePattern); 96 | 97 | // compiled pattern #^/item-(?([1-9][0-9]*)|0)$#" 98 | $exploded = \explode($token, $toCompile, 2); 99 | $compiledParts[] = \preg_quote($exploded[0], Patterns::DELIMITER); 100 | $compiledParts[] = "(?<{$name}>{$pattern})"; 101 | $toCompile = $exploded[1]; 102 | 103 | $explodedParams[$name] = [ 104 | $default, 105 | Patterns::DELIMITER . '^(' . $pattern . ')$' . Patterns::DELIMITER, 106 | $patternName 107 | ]; 108 | } 109 | $compiledParts[] = \preg_quote($toCompile, Patterns::DELIMITER); 110 | $d = Patterns::DELIMITER; 111 | $compiledPattern = $d . '^' . \implode('', $compiledParts) . '$' . $d; 112 | 113 | $this->simplePattern = $simplePattern; 114 | $this->compiledPattern = $compiledPattern; 115 | $this->explodedParams = $explodedParams; 116 | } 117 | 118 | private function replaceFirst(string $search, string $replace, string $subject): string 119 | { 120 | $exploded = \explode($search, $subject, 2); 121 | 122 | return $exploded[0] . $replace . $exploded[1]; 123 | } 124 | 125 | /** 126 | * Validates, change pattern name to regex and add pattern's name or null 127 | * 128 | * e.g. ['{{ month :int }}', 'name', '(-?[1-9][0-9]*)|0', null, ':int'] 129 | * 130 | * @return \Generator 131 | */ 132 | private function processTokens() 133 | { 134 | foreach ($this->getTokensStream() as list($string, $name, $pattern, $default)) { 135 | if (!\preg_match('/^[a-zA-Z0-9_]+$/', $name)) { 136 | throw new InvalidArgumentException("Invalid param name “{$name}” (source: {$this->pattern})"); 137 | } 138 | 139 | Patterns::validatePatternName($name); 140 | 141 | $patternName = null; 142 | if ($patternObj = $this->patterns[$pattern] ?? null) { 143 | $patternName = $pattern; 144 | /** @var PatternInterface $patternObj */ 145 | $pattern = $patternObj->getRegex(); 146 | } 147 | 148 | if (!(new RegexTester())->isSubregex($pattern)) { 149 | throw new InvalidArgumentException("Invalid regex: {$pattern} (source: {$this->pattern})"); 150 | } 151 | 152 | yield [ 153 | $string, 154 | $name, 155 | $pattern, 156 | $default, 157 | $patternName, 158 | ]; 159 | } 160 | } 161 | 162 | /** 163 | * e.g. [ 164 | * // [text, name, pattern, default] 165 | * ['{{ month :int }}', 'month', ':int', null], 166 | * ] 167 | * 168 | * @return \Generator 169 | */ 170 | private function getTokensStream() 171 | { 172 | $matches = []; 173 | \preg_match_all(static::PATTERN_VAR, $this->pattern, $matches); 174 | foreach ($matches[0] ?? [] as $match) { 175 | $arr = $this->paramStrToArr($match); 176 | 177 | if (\count($arr) > 3) { 178 | throw new InvalidArgumentException("Invalid url pattern {$this->pattern}"); 179 | } 180 | 181 | $name = $arr[0]; 182 | $pattern = $arr[1] ?? $this->patterns->getDefaultPattern(); 183 | $default = $arr[2] ?? null; 184 | 185 | yield [ 186 | $match, 187 | $name, 188 | $pattern, 189 | $default 190 | ]; 191 | } 192 | } 193 | 194 | /** 195 | * @param string $paramString e.g. {{ id :int }} 196 | * 197 | * @return string[] e.g. ['id', 'int'] 198 | */ 199 | private function paramStrToArr(string $paramString) 200 | { 201 | $str = \substr($paramString, 2, -2); 202 | $result = \array_filter(\preg_split('/\\s+/', $str), function ($a) { 203 | return '' !== \trim($a); 204 | }); 205 | $result = \array_values($result); 206 | 207 | if (!\in_array(\strpos($result[0], ':'), [0, false], true)) { 208 | $first = \array_shift($result); 209 | list($a, $b) = \explode(':', $first, 2); 210 | $b = ':' . $b; 211 | \array_unshift($result, $a, $b); 212 | } 213 | 214 | return $result; 215 | } 216 | 217 | public function match(string $path, &$params): bool 218 | { 219 | if ($result = (bool) \preg_match($this->compiledPattern, $path, $matches)) { 220 | $resultParams = \array_filter( 221 | $matches, 222 | function ($key) { 223 | return !\is_int($key); 224 | }, 225 | ARRAY_FILTER_USE_KEY 226 | ); 227 | 228 | foreach ($resultParams as $key => $value) { 229 | $patternName = $this->explodedParams[$key][2]; 230 | if (!\is_null($patternName)) { 231 | $pattern = $this->patterns[$patternName]; 232 | try { 233 | $resultParams[$key] = $pattern->fromUrl($value); 234 | } catch (PatternException $exception) { 235 | return false; 236 | } 237 | } 238 | } 239 | 240 | $params = $resultParams; 241 | 242 | return true; 243 | } 244 | 245 | return false; 246 | } 247 | 248 | /** 249 | * Returns false or converted params 250 | * 251 | * @param string[] $params 252 | * 253 | * @return bool|array 254 | */ 255 | public function matchParams(array $params) 256 | { 257 | $result = []; 258 | foreach ($this->explodedParams as $name => list($default, $pattern, $patternName)) { 259 | if (!\array_key_exists($name, $params)) { 260 | if (\is_null($default)) { 261 | return false; 262 | } 263 | $currentParam = $default; 264 | } else { 265 | $currentParam = $params[$name]; 266 | } 267 | 268 | if (!\is_null($patternName)) { 269 | $patternObj = $this->patterns[$patternName]; 270 | 271 | try { 272 | $result[$name] = $patternObj->toUrl($currentParam); 273 | } catch (PatternException $exception) { 274 | return false; 275 | } 276 | } else { 277 | if (\is_object($currentParam) && \method_exists($currentParam, '__toString')) { 278 | $currentParam = (string) $currentParam; 279 | } 280 | if (!$this->pregMatchMultiType($pattern, $currentParam)) { 281 | return false; 282 | } 283 | $result[$name] = $currentParam; 284 | } 285 | } 286 | 287 | return $result; 288 | } 289 | 290 | public function bindParams(array $params): LinkInterface 291 | { 292 | $result = $this->simplePattern; 293 | foreach ($this->explodedParams as $name => list($default)) { 294 | $value = $params[$name] ?? $default; 295 | $result = \str_replace('{{' . $name . '}}', $value, $result); 296 | unset($params[$name]); 297 | } 298 | 299 | return (new Link($result))->withParams($params); 300 | } 301 | 302 | /** 303 | * @return PatternRouteNode[] 304 | */ 305 | public function getNodes(): array 306 | { 307 | $result = []; 308 | $pattern = $this->pattern; 309 | 310 | while (\strlen($pattern) > 0) { 311 | if ('{{' === \substr($pattern, 0, 2)) { 312 | $result[] = new PatternRouteNode($pattern, true); 313 | break; 314 | } 315 | 316 | $result[] = new PatternRouteNode(\substr($pattern, 0, 1), false); 317 | $pattern = \substr($pattern, 1); 318 | } 319 | 320 | return $result; 321 | } 322 | 323 | private function pregMatchMultiType(string $pattern, $subject): int 324 | { 325 | if ( 326 | \is_string($subject) 327 | || \is_scalar($subject) 328 | || \is_null($subject) 329 | ) { 330 | return \preg_match($pattern, (string) $subject); 331 | } 332 | 333 | return 0; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/Pattern/PatternRouteNode.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\ExportableTrait; 13 | 14 | /** 15 | * @internal 16 | */ 17 | class PatternRouteNode 18 | { 19 | use ExportableTrait; 20 | 21 | private $key; 22 | 23 | private $regex; 24 | 25 | public function __construct(string $key, bool $isRegex) 26 | { 27 | $this->key = $key; 28 | $this->regex = $isRegex; 29 | } 30 | 31 | public function isRegex(): bool 32 | { 33 | return $this->regex; 34 | } 35 | 36 | public function getKey(): string 37 | { 38 | return $this->key; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Pattern/PatternRouter.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\Exceptions\HttpException; 13 | use Awesomite\Chariot\Exceptions\InvalidArgumentException; 14 | use Awesomite\Chariot\Exceptions\LogicException; 15 | use Awesomite\Chariot\ExportableTrait; 16 | use Awesomite\Chariot\HttpMethods; 17 | use Awesomite\Chariot\InternalRoute; 18 | use Awesomite\Chariot\InternalRouteInterface; 19 | use Awesomite\Chariot\LinkInterface; 20 | use Awesomite\Chariot\ParamDecorators\ParamDecoratorInterface; 21 | use Awesomite\Chariot\RouterInterface; 22 | 23 | class PatternRouter implements RouterInterface 24 | { 25 | const STRATEGY_SEQUENTIALLY = 1; 26 | const STRATEGY_TREE = 2; 27 | 28 | use ExportableTrait; 29 | 30 | /** 31 | * Example: 32 | * list($patternRoute, $extraParams) = $routes['GET'][$handler][0]; 33 | * 34 | * @var array 35 | */ 36 | private $routes = []; 37 | 38 | /** 39 | * Example: 40 | * list($handler, $extraParams) = $routes['GET']['/contact']; 41 | * 42 | * @var array 43 | */ 44 | private $keyValueRoutes = []; 45 | 46 | private $nodesTree = []; 47 | 48 | private $strategy; 49 | 50 | /** 51 | * @var PatternsInterface 52 | */ 53 | private $patterns; 54 | 55 | private $paramDecorators; 56 | 57 | private $frozen = false; 58 | 59 | public function __construct(PatternsInterface $patterns, int $strategy = self::STRATEGY_TREE) 60 | { 61 | $this->patterns = $patterns; 62 | if (!\in_array($strategy, [static::STRATEGY_TREE, static::STRATEGY_SEQUENTIALLY], true)) { 63 | throw new InvalidArgumentException("Invalid strategy: {$strategy}"); 64 | } 65 | $this->strategy = $strategy; 66 | } 67 | 68 | public static function createDefault(): self 69 | { 70 | return new static(Patterns::createDefault()); 71 | } 72 | 73 | public function getPatterns(): PatternsInterface 74 | { 75 | return $this->patterns; 76 | } 77 | 78 | public function addParamDecorator(ParamDecoratorInterface $decorator): self 79 | { 80 | $this->getParamDecorators()->attach($decorator); 81 | 82 | return $this; 83 | } 84 | 85 | private $requiredParams = []; 86 | 87 | private function getDiff(array $a, array $b) 88 | { 89 | $diff = []; 90 | foreach ($a as $el) { 91 | if (!\in_array($el, $b)) { 92 | $diff[] = '-' . $el; 93 | } 94 | } 95 | foreach ($b as $el) { 96 | if (!\in_array($el, $a)) { 97 | $diff[] = '+' . $el; 98 | } 99 | } 100 | 101 | return $diff; 102 | } 103 | 104 | private function validateRouteParams(PatternRoute $route, string $method, string $pattern, string $handler, array $extraParams) 105 | { 106 | $requiredParams = \array_merge($route->getRequiredParams(), \array_keys($extraParams)); 107 | if (!isset($this->requiredParams[$handler])) { 108 | $this->requiredParams[$handler] = [$method, $requiredParams, $pattern, $extraParams]; 109 | return; 110 | } 111 | 112 | list($oldMethod, $oldRequiredParams, $oldPattern, $oldExtraParams) = $this->requiredParams[$handler]; 113 | $diff = $this->getDiff($oldRequiredParams, $requiredParams); 114 | if ($diff) { 115 | $message = <<<'ERROR' 116 | Each route associated with the same handler must contain the same parameters 117 | 118 | 1) %s `%s`%s 119 | [%s] 120 | 2) %s `%s`%s 121 | [%s] 122 | 123 | Diff: %s 124 | ERROR; 125 | 126 | throw new InvalidArgumentException(\sprintf( 127 | $message, 128 | 129 | $oldMethod, 130 | $oldPattern, 131 | $oldExtraParams ? ' [' . \http_build_query($oldExtraParams) . ']' : '', 132 | \implode(', ', $oldRequiredParams), 133 | 134 | $method, 135 | $pattern, 136 | $extraParams ? ' [' . \http_build_query($extraParams) . ']' : '', 137 | \implode(', ', $requiredParams), 138 | 139 | \implode(', ', $diff) 140 | )); 141 | } 142 | } 143 | 144 | public function addRoute(string $method, string $pattern, string $handler, array $extraParams = []): self 145 | { 146 | if ($this->frozen) { 147 | throw new LogicException(\sprintf('Object `%s` is frozen, cannot add new routes', static::class)); 148 | } 149 | 150 | $this->processExtraParams($extraParams); 151 | 152 | if (!\in_array($method, HttpMethods::ALL_METHODS, true)) { 153 | throw new InvalidArgumentException( 154 | \sprintf( 155 | 'Method is equal to %s, but must be equal to one of the following: %s', 156 | $method, 157 | \implode(', ', HttpMethods::ALL_METHODS) 158 | ) 159 | ); 160 | } 161 | 162 | $route = new PatternRoute($pattern, $this->patterns); 163 | $this->validateRouteParams($route, $method, $pattern, $handler, $extraParams); 164 | $this->routes[$method][$handler][] = [$route, $extraParams]; 165 | 166 | if (false === \strpos($pattern, '{{')) { 167 | $this->keyValueRoutes[$method][$pattern] = [$handler, $extraParams]; 168 | } elseif ($this->strategy === static::STRATEGY_TREE) { 169 | $currentNode = &$this->nodesTree; 170 | foreach ($route->getNodes() as $node) { 171 | if ($node->isRegex()) { 172 | break; 173 | } 174 | if (!isset($currentNode[$node->getKey()])) { 175 | $currentNode[$node->getKey()] = []; 176 | } 177 | $currentNode = &$currentNode[$node->getKey()]; 178 | } 179 | 180 | /** @var PatternRouteNode $node */ 181 | $lastKey = $node->isRegex() ? 'regex' : 'all'; 182 | $currentNode[$lastKey][] = [ 183 | $route, 184 | $handler, 185 | $extraParams, 186 | $method, 187 | ]; 188 | } 189 | 190 | return $this; 191 | } 192 | 193 | public function any(string $pattern, string $handler, array $extraParams = []): self 194 | { 195 | return $this->addRoute(HttpMethods::METHOD_ANY, $pattern, $handler, $extraParams); 196 | } 197 | 198 | public function get(string $pattern, string $handler, array $extraParams = []): self 199 | { 200 | return $this->addRoute(HttpMethods::METHOD_GET, $pattern, $handler, $extraParams); 201 | } 202 | 203 | public function post(string $pattern, string $handler, array $extraParams = []): self 204 | { 205 | return $this->addRoute(HttpMethods::METHOD_POST, $pattern, $handler, $extraParams); 206 | } 207 | 208 | public function put(string $pattern, string $handler, array $extraParams = []): self 209 | { 210 | return $this->addRoute(HttpMethods::METHOD_PUT, $pattern, $handler, $extraParams); 211 | } 212 | 213 | public function delete(string $pattern, string $handler, array $extraParams = []): self 214 | { 215 | return $this->addRoute(HttpMethods::METHOD_DELETE, $pattern, $handler, $extraParams); 216 | } 217 | 218 | public function patch(string $pattern, string $handler, array $extraParams = []): self 219 | { 220 | return $this->addRoute(HttpMethods::METHOD_PATCH, $pattern, $handler, $extraParams); 221 | } 222 | 223 | public function connect(string $pattern, string $handler, array $extraParams = []): self 224 | { 225 | return $this->addRoute(HttpMethods::METHOD_CONNECT, $pattern, $handler, $extraParams); 226 | } 227 | 228 | public function options(string $pattern, string $handler, array $extraParams = []): self 229 | { 230 | return $this->addRoute(HttpMethods::METHOD_OPTIONS, $pattern, $handler, $extraParams); 231 | } 232 | 233 | public function trace(string $pattern, string $handler, array $extraParams = []): self 234 | { 235 | return $this->addRoute(HttpMethods::METHOD_TRACE, $pattern, $handler, $extraParams); 236 | } 237 | 238 | public function match(string $method, string $path): InternalRouteInterface 239 | { 240 | switch ($this->strategy) { 241 | case static::STRATEGY_SEQUENTIALLY: 242 | return $this->matchSequentially($method, $path); 243 | 244 | case static::STRATEGY_TREE: 245 | return $this->matchTree($method, $path); 246 | } 247 | // @codeCoverageIgnoreStart 248 | } 249 | 250 | // @codeCoverageIgnoreEnd 251 | 252 | public function getAllowedMethods(string $url): array 253 | { 254 | $allRealMethods = \array_diff(HttpMethods::ALL_METHODS, [HttpMethods::METHOD_ANY]); 255 | 256 | 257 | switch ($this->strategy) { 258 | case static::STRATEGY_SEQUENTIALLY: 259 | if ($this->matchSequentiallyForMethods([HttpMethods::METHOD_ANY], $url)) { 260 | return $allRealMethods; 261 | } 262 | break; 263 | 264 | case static::STRATEGY_TREE: 265 | if ($this->matchTreeForMethods([HttpMethods::METHOD_ANY], $url)) { 266 | return $allRealMethods; 267 | } 268 | break; 269 | } 270 | 271 | $result = []; 272 | foreach (\array_diff($allRealMethods, [HttpMethods::METHOD_HEAD]) as $method) { 273 | switch ($this->strategy) { 274 | case static::STRATEGY_SEQUENTIALLY: 275 | if ($this->matchSequentiallyForMethods([$method], $url)) { 276 | $result[] = $method; 277 | if (HttpMethods::METHOD_GET === $method) { 278 | $result[] = HttpMethods::METHOD_HEAD; 279 | } 280 | } 281 | break; 282 | 283 | case static::STRATEGY_TREE: 284 | if ($this->matchTreeForMethods([$method], $url)) { 285 | $result[] = $method; 286 | if (HttpMethods::METHOD_GET === $method) { 287 | $result[] = HttpMethods::METHOD_HEAD; 288 | } 289 | } 290 | break; 291 | } 292 | } 293 | 294 | return $result; 295 | } 296 | 297 | /** 298 | * @param string $path 299 | * @param array $methods 300 | * 301 | * @return InternalRouteInterface|null 302 | */ 303 | private function matchKeyValue(string $path, array $methods) 304 | { 305 | foreach ($methods as $method) { 306 | $keyValue = $this->keyValueRoutes[$method][$path] ?? null; 307 | if (!\is_null($keyValue)) { 308 | list($handler, $extraParams) = $keyValue; 309 | 310 | return new InternalRoute($handler, $extraParams); 311 | } 312 | } 313 | 314 | return null; 315 | } 316 | 317 | private function methodMapping(string $method): array 318 | { 319 | switch ($method) { 320 | case HttpMethods::METHOD_ANY: 321 | return HttpMethods::ALL_METHODS; 322 | 323 | case HttpMethods::METHOD_HEAD: 324 | return [ 325 | HttpMethods::METHOD_ANY, 326 | HttpMethods::METHOD_GET, 327 | HttpMethods::METHOD_HEAD, 328 | ]; 329 | 330 | default: 331 | return [$method, HttpMethods::METHOD_ANY]; 332 | } 333 | } 334 | 335 | private function matchSequentially(string $method, string $path): InternalRouteInterface 336 | { 337 | $methods = $this->methodMapping($method); 338 | 339 | if ($result = $this->matchSequentiallyForMethods($methods, $path)) { 340 | return $result; 341 | } 342 | 343 | if ($this->matchSequentiallyForMethods(\array_diff(HttpMethods::ALL_METHODS, $methods), $path)) { 344 | throw new HttpException($method, $path, HttpException::HTTP_METHOD_NOT_ALLOWED); 345 | } 346 | 347 | throw new HttpException($method, $path, HttpException::HTTP_NOT_FOUND); 348 | } 349 | 350 | /** 351 | * @param array $methods 352 | * @param string $path 353 | * 354 | * @return InternalRouteInterface|null 355 | */ 356 | private function matchSequentiallyForMethods(array $methods, string $path) 357 | { 358 | if ($result = $this->matchKeyValue($path, $methods)) { 359 | return $result; 360 | } 361 | 362 | foreach ($methods as $currentMethod) { 363 | foreach ($this->routes[$currentMethod] ?? [] as $handler => $handlerData) { 364 | foreach ($handlerData as list($patternRoute, $extraParams)) { 365 | /** @var PatternRoute $patternRoute */ 366 | /** @var array $extraParams */ 367 | if ($patternRoute->match($path, $queryParams)) { 368 | return new InternalRoute($handler, \array_replace($extraParams, $queryParams)); 369 | } 370 | } 371 | } 372 | } 373 | 374 | return null; 375 | } 376 | 377 | private function matchTree(string $method, string $path): InternalRouteInterface 378 | { 379 | $methods = $this->methodMapping($method); 380 | 381 | if ($result = $this->matchTreeForMethods($methods, $path)) { 382 | return $result; 383 | } 384 | 385 | if ($this->matchTreeForMethods(\array_diff(HttpMethods::ALL_METHODS, $methods), $path)) { 386 | throw new HttpException($method, $path, HttpException::HTTP_METHOD_NOT_ALLOWED); 387 | } 388 | 389 | throw new HttpException($method, $path, HttpException::HTTP_NOT_FOUND); 390 | } 391 | 392 | /** 393 | * @param array $methods 394 | * @param string $path 395 | * 396 | * @return InternalRouteInterface|null 397 | */ 398 | private function matchTreeForMethods(array $methods, string $path) 399 | { 400 | if ($result = $this->matchKeyValue($path, $methods)) { 401 | return $result; 402 | } 403 | 404 | $nodesPointer = &$this->nodesTree; 405 | $chars = \str_split($path); 406 | 407 | while (true) { 408 | $candidates = \array_merge( 409 | $nodesPointer['regex'] ?? [], 410 | $nodesPointer['all'] ?? [] 411 | ); 412 | 413 | foreach ($candidates as list($route, $handler, $params, $method)) { 414 | if (!\in_array($method, $methods, true)) { 415 | continue; 416 | } 417 | /** @var PatternRoute $route */ 418 | if ($route->match($path, $queryParams)) { 419 | return new InternalRoute($handler, \array_replace($params, $queryParams)); 420 | } 421 | } 422 | 423 | $char = \array_shift($chars); 424 | if (!\is_string($char) || !isset($nodesPointer[$char])) { 425 | break; 426 | } 427 | $nodesPointer = &$nodesPointer[$char]; 428 | } 429 | 430 | return null; 431 | } 432 | 433 | public function linkTo(string $handler, string $method = HttpMethods::METHOD_ANY): LinkInterface 434 | { 435 | $routes = []; 436 | 437 | foreach ($this->methodMapping($method) as $currentMethod) { 438 | $routes = \array_merge($routes, $this->routes[$currentMethod][$handler] ?? []); 439 | } 440 | 441 | list(, $required) = $this->requiredParams[$handler] ?? [null, []]; 442 | return new PatternLink($method, $handler, $routes, $this->getParamDecorators(), $required); 443 | } 444 | 445 | /** 446 | * Export to executable php code 447 | * 448 | * Example: 449 | * file_put_contents('router.cache', 'export() . ';'); 450 | * $router = require 'router.cache'; 451 | * 452 | * @return string 453 | */ 454 | public function exportToExecutable(): string 455 | { 456 | return (new SourceCodeExporter())->exportPatternRouter($this); 457 | } 458 | 459 | private function getParamDecorators(): \SplObjectStorage 460 | { 461 | return $this->paramDecorators ?? $this->paramDecorators = new \SplObjectStorage(); 462 | } 463 | 464 | private function processExtraParams(array &$data) 465 | { 466 | \array_walk_recursive($data, function (&$element) { 467 | if (\is_scalar($element) || \is_null($element)) { 468 | return; 469 | } 470 | 471 | if (\is_object($element)) { 472 | if ($element instanceof \Traversable) { 473 | $element = \iterator_to_array($element); 474 | $this->processExtraParams($element); 475 | 476 | return; 477 | } 478 | 479 | if (\method_exists($element, '__toString')) { 480 | $element = (string) $element; 481 | 482 | return; 483 | } 484 | } 485 | 486 | $message = \sprintf( 487 | 'Additional parameters can contain only scalar or null values, "%s" given', 488 | \is_object($element) ? \get_class($element) : \gettype($element) 489 | ); 490 | 491 | throw new InvalidArgumentException($message); 492 | }); 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /src/Pattern/Patterns.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\Exceptions\InvalidArgumentException; 13 | use Awesomite\Chariot\Exceptions\LogicException; 14 | use Awesomite\Chariot\Pattern\StdPatterns\DatePattern; 15 | use Awesomite\Chariot\Pattern\StdPatterns\FloatPattern; 16 | use Awesomite\Chariot\Pattern\StdPatterns\IntPattern; 17 | use Awesomite\Chariot\Pattern\StdPatterns\Ip4Pattern; 18 | use Awesomite\Chariot\Pattern\StdPatterns\ListPattern; 19 | use Awesomite\Chariot\Pattern\StdPatterns\RegexPattern; 20 | use Awesomite\Chariot\Pattern\StdPatterns\UnsignedFloatPattern; 21 | use Awesomite\Chariot\Pattern\StdPatterns\UnsignedIntPattern; 22 | 23 | class Patterns implements PatternsInterface 24 | { 25 | const DELIMITER = '#'; 26 | 27 | const REGEX_INT = '(-?[1-9][0-9]*)|0'; 28 | const REGEX_UINT = '([1-9][0-9]*)|0'; 29 | const REGEX_FLOAT = '((-?[1-9][0-9]*)|0)(\.[0-9]*[1-9]+)?'; 30 | const REGEX_UFLOAT = '(([1-9][0-9]*)|0)(\.[0-9]*[1-9]+)?'; 31 | const REGEX_ALPHANUM = '[a-zA-Z0-9]+'; 32 | const REGEX_DATE = '[0-9]{4}-[0-9]{2}-[0-9]{2}'; 33 | const REGEX_IP = '((25[0-5])|(2[0-4][0-9])|(1[0-9][0-9])|([1-9]?[0-9]))(\.((25[0-5])|(2[0-4][0-9])|(1[0-9][0-9])|([1-9]?[0-9]))){3}'; 34 | const REGEX_DEFAULT = '[^/]+'; 35 | 36 | const STANDARD_PATTERNS 37 | = [ 38 | ':int' => IntPattern::class, 39 | ':uint' => UnsignedIntPattern::class, 40 | ':float' => FloatPattern::class, 41 | ':ufloat' => UnsignedFloatPattern::class, 42 | ':date' => DatePattern::class, 43 | ':list' => ListPattern::class, 44 | ':ip4' => Ip4Pattern::class, 45 | ':alphanum' => self::REGEX_ALPHANUM, 46 | ]; 47 | 48 | private $patterns = []; 49 | 50 | private $defaultPattern; 51 | 52 | private $frozen = false; 53 | 54 | /** 55 | * @internal 56 | * @param string $name 57 | */ 58 | public static function validatePatternName(string $name) 59 | { 60 | $max = 32; 61 | if (\strlen($name) > $max) { 62 | throw new InvalidArgumentException(\sprintf( 63 | 'Compilation failed: subpattern name is too long (maximum %d characters) [%s]', 64 | $max, 65 | $name 66 | )); 67 | } 68 | } 69 | 70 | public function __construct(array $patterns = [], string $defaultPattern = null) 71 | { 72 | foreach ($patterns as $name => $pattern) { 73 | if (\is_array($pattern)) { 74 | $this->addEnumPattern($name, $pattern); 75 | } else { 76 | $this->addPattern($name, $pattern); 77 | } 78 | } 79 | 80 | $this->setDefaultPattern(\is_null($defaultPattern) ? static::REGEX_DEFAULT : $defaultPattern); 81 | } 82 | 83 | public function addPattern(string $name, $pattern): PatternsInterface 84 | { 85 | if ($this->frozen) { 86 | throw new LogicException(\sprintf('Object `%s` is frozen, cannot add new patterns', static::class)); 87 | } 88 | 89 | static::validatePatternName($name); 90 | 91 | if (isset($this[$name])) { 92 | throw new LogicException(\sprintf('Pattern %s is already added', $name)); 93 | } 94 | 95 | if (':' !== ($name[0] ?? null)) { 96 | throw new InvalidArgumentException(\sprintf( 97 | 'Method %s() requires first parameter prefixed by ":", "%s" given', 98 | __METHOD__, 99 | $name 100 | )); 101 | } 102 | 103 | if ( 104 | \is_string($pattern) 105 | || (\is_object($pattern) && \method_exists($pattern, '__toString') && !$pattern instanceof PatternInterface) 106 | ) { 107 | $pattern = new RegexPattern((string) $pattern); 108 | } 109 | 110 | if (\is_object($pattern) && $pattern instanceof PatternInterface) { 111 | if (!(new RegexTester())->isSubregex($pattern->getRegex())) { 112 | throw new InvalidArgumentException('Invalid regex: ' . $pattern->getRegex()); 113 | } 114 | $this->patterns[$name] = $pattern; 115 | } else { 116 | throw new InvalidArgumentException(\sprintf( 117 | 'Method %s() expects string or %s, %s given', 118 | __METHOD__, 119 | PatternInterface::class, 120 | \is_object($pattern) ? \get_class($pattern) : \gettype($pattern) 121 | )); 122 | } 123 | 124 | return $this; 125 | } 126 | 127 | public static function createDefault(): self 128 | { 129 | $result = new self(); 130 | foreach (self::STANDARD_PATTERNS as $name => $pattern) { 131 | if (\class_exists($pattern)) { 132 | $pattern = new $pattern(); 133 | } 134 | $result->addPattern($name, $pattern); 135 | } 136 | 137 | return $result; 138 | } 139 | 140 | public function addEnumPattern(string $name, array $values): PatternsInterface 141 | { 142 | $processed = []; 143 | foreach ($values as $value) { 144 | $processed[] = \preg_quote($value, static::DELIMITER); 145 | } 146 | 147 | return $this->addPattern($name, \implode('|', $processed)); 148 | } 149 | 150 | public function getDefaultPattern() 151 | { 152 | return $this->defaultPattern; 153 | } 154 | 155 | public function setDefaultPattern($pattern): PatternsInterface 156 | { 157 | if (!(new RegexTester())->isSubregex($pattern)) { 158 | throw new InvalidArgumentException('Invalid regex: ' . $pattern); 159 | } 160 | $this->defaultPattern = $pattern; 161 | 162 | return $this; 163 | } 164 | 165 | public function offsetExists($offset) 166 | { 167 | return \array_key_exists($offset, $this->patterns); 168 | } 169 | 170 | public function offsetGet($offset) 171 | { 172 | return $this->patterns[$offset]; 173 | } 174 | 175 | public function offsetSet($offset, $value) 176 | { 177 | $this->addPattern($offset, $value); 178 | } 179 | 180 | public function offsetUnset($offset) 181 | { 182 | throw new LogicException('Operation forbidden'); 183 | } 184 | 185 | public function serialize() 186 | { 187 | return \serialize([ 188 | 'patterns' => $this->patterns, 189 | 'defaultPattern' => $this->defaultPattern, 190 | ]); 191 | } 192 | 193 | public function unserialize($serialized) 194 | { 195 | $data = \unserialize($serialized); 196 | $this->patterns = $data['patterns']; 197 | $this->defaultPattern = $data['defaultPattern']; 198 | $this->frozen = true; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Pattern/PatternsInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\Exceptions\InvalidArgumentException; 13 | 14 | interface PatternsInterface extends \ArrayAccess, \Serializable 15 | { 16 | /** 17 | * @param string $name 18 | * @param string|PatternInterface $pattern Acceptable also stringable object (with method __toString) 19 | * 20 | * @return self 21 | * 22 | * @throws InvalidArgumentException 23 | */ 24 | public function addPattern(string $name, $pattern): self; 25 | 26 | /** 27 | * @param string $name 28 | * @param string[] $values 29 | * 30 | * @return self 31 | * 32 | * @throws InvalidArgumentException 33 | */ 34 | public function addEnumPattern(string $name, array $values): self; 35 | 36 | /** 37 | * @param string|PatternInterface $pattern 38 | * 39 | * @return self 40 | * 41 | * @throws InvalidArgumentException 42 | */ 43 | public function setDefaultPattern($pattern): self; 44 | 45 | public function getDefaultPattern(); 46 | 47 | /** 48 | * @param mixed $offset 49 | * 50 | * @return PatternInterface 51 | */ 52 | public function offsetGet($offset); 53 | } 54 | -------------------------------------------------------------------------------- /src/Pattern/RegexTester.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | /** 13 | * @internal 14 | */ 15 | class RegexTester 16 | { 17 | public function isSubregex(string $subregex, string $delimiter = Patterns::DELIMITER): bool 18 | { 19 | return $this->isRegex($delimiter . $subregex . $delimiter); 20 | } 21 | 22 | public function isRegex(string $regex): bool 23 | { 24 | \set_error_handler(function () { 25 | }, E_ALL); 26 | $test = @\preg_match($regex, ''); 27 | \restore_error_handler(); 28 | 29 | return false !== $test; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Pattern/SourceCodeExporter.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern; 11 | 12 | use Awesomite\Chariot\Reflections\Objects; 13 | 14 | /** 15 | * @internal 16 | */ 17 | class SourceCodeExporter 18 | { 19 | public function exportPatternRouter(PatternRouter $router): string 20 | { 21 | $template 22 | = <<<'TEMPLATE' 23 | (function () { 24 | $patterns = \unserialize([[patterns]]); 25 | 26 | $routes = [[routes]]; 27 | 28 | return \Awesomite\Chariot\Pattern\PatternRouter::__set_state(array( 29 | 'patterns' => $patterns, 30 | 'keyValueRoutes' => [[keyValueRoutes]], 31 | 'nodesTree' => [[nodesTree]], 32 | 'strategy' => [[strategy]], 33 | 'routes' => $routes, 34 | 'requiredParams' => [[requiredParams]], 35 | 'frozen' => true, 36 | )); 37 | })() 38 | 39 | TEMPLATE; 40 | 41 | $routes = Objects::getProperty($router, 'routes'); 42 | $exportedRoutes = ''; 43 | $this->exportRoutes($routes, '$patterns', $exportedRoutes); 44 | 45 | $nodesTree = Objects::getProperty($router, 'nodesTree'); 46 | $exportedNodes = ''; 47 | $this->exportRoutes($nodesTree, '$patterns', $exportedNodes); 48 | 49 | $replace = [ 50 | '[[routes]]' => $exportedRoutes, 51 | '[[patterns]]' => \var_export(\serialize(Objects::getProperty($router, 'patterns')), true), 52 | '[[keyValueRoutes]]' => $this->varExportFromObject($router, 'keyValueRoutes'), 53 | '[[nodesTree]]' => $exportedNodes, 54 | '[[strategy]]' => $this->varExportFromObject($router, 'strategy'), 55 | '[[requiredParams]]' => $this->varExportFromObject($router, 'requiredParams'), 56 | ]; 57 | 58 | return \str_replace(\array_keys($replace), \array_values($replace), $template); 59 | } 60 | 61 | private function exportRoutes( 62 | array $routes, 63 | string $patternsName, 64 | string &$result, 65 | string $indent = '' 66 | ) { 67 | $result .= "{$indent}array(\n"; 68 | foreach ($routes as $key => $value) { 69 | $result .= "{$indent} "; 70 | $result .= \is_int($key) ? $key : \var_export((string) $key, true); 71 | $result .= ' => '; 72 | if (\is_array($value)) { 73 | $result .= "\n"; 74 | $this->exportRoutes($value, $patternsName, $result, $indent . ' '); 75 | } else { 76 | if (\is_object($value) && $value instanceof PatternRoute) { 77 | $result .= $this->exportRoute($value, $patternsName, $indent . ' '); 78 | } else { 79 | // @codeCoverageIgnoreStart 80 | $result .= \var_export($value, true); 81 | // codeCoverageIgnoreEnd 82 | } 83 | } 84 | $result .= ",\n"; 85 | } 86 | $result .= "{$indent})"; 87 | } 88 | 89 | private function exportRoute(PatternRoute $route, $patternsName, string $indent): string 90 | { 91 | $template 92 | = <<<'TEMPLATE' 93 | \Awesomite\Chariot\Pattern\PatternRoute::__set_state(array( 94 | 'pattern' => [[pattern]], 95 | 'compiledPattern' => [[compiledPattern]], 96 | 'simplePattern' => [[simplePattern]], 97 | 'explodedParams' => [[explodedParams]], 98 | 'patterns' => [[patterns]], 99 | 'frozen' => true, 100 | )) 101 | TEMPLATE; 102 | $template = \str_replace("\n", "\n{$indent}", $template); 103 | 104 | $data = [ 105 | '[[pattern]]' => $this->varExportFromObject($route, 'pattern'), 106 | '[[compiledPattern]]' => $this->varExportFromObject($route, 'compiledPattern'), 107 | '[[simplePattern]]' => $this->varExportFromObject($route, 'simplePattern'), 108 | '[[explodedParams]]' => $this->varExportFromObject($route, 'explodedParams'), 109 | '[[patterns]]' => $patternsName, 110 | ]; 111 | 112 | return \str_replace(\array_keys($data), \array_values($data), $template); 113 | } 114 | 115 | private function varExportFromObject($object, string $propertyName): string 116 | { 117 | return \var_export(Objects::getProperty($object, $propertyName), true); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/AbstractPattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | use Awesomite\Chariot\Exceptions\PatternException; 13 | use Awesomite\Chariot\Pattern\PatternInterface; 14 | use Awesomite\Chariot\Pattern\Patterns; 15 | 16 | abstract class AbstractPattern implements PatternInterface 17 | { 18 | public function serialize() 19 | { 20 | return ''; 21 | } 22 | 23 | public function unserialize($serialized) 24 | { 25 | } 26 | 27 | /** 28 | * @param $data 29 | * 30 | * @return PatternException 31 | */ 32 | protected function newInvalidToUrl($data) 33 | { 34 | switch (\gettype($data)) { 35 | case 'string': 36 | $type = \sprintf('(string) %s', \var_export($data, true)); 37 | break; 38 | 39 | case 'object': 40 | $type = \sprintf('(object) %s', \get_class($data)); 41 | break; 42 | 43 | case 'integer': 44 | case 'double': 45 | case 'float': 46 | $type = \sprintf('(%s) %s', \gettype($data), \var_export($data, true)); 47 | break; 48 | 49 | default: 50 | $type = \gettype($data); 51 | break; 52 | } 53 | 54 | return new PatternException( 55 | \sprintf('Value %s cannot be converted to url param (%s)', $type, static::class), 56 | PatternException::CODE_TO_URL 57 | ); 58 | } 59 | 60 | /** 61 | * @param string $param 62 | * 63 | * @return PatternException 64 | */ 65 | protected function newInvalidFromUrl(string $param) 66 | { 67 | return new PatternException( 68 | \sprintf('Value %s is invalid (%s)', $param, static::class), 69 | PatternException::CODE_FROM_URL 70 | ); 71 | } 72 | 73 | protected function match(string $data): bool 74 | { 75 | $d = Patterns::DELIMITER; 76 | 77 | return (bool) \preg_match($d . '^(' . $this->getRegex() . ')$' . $d, $data); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/DatePattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | use Awesomite\Chariot\Exceptions\PatternException; 13 | use Awesomite\Chariot\Pattern\Patterns; 14 | 15 | class DatePattern extends AbstractPattern 16 | { 17 | const DATE_FORMAT = 'Y-m-d'; 18 | 19 | /** 20 | * @var \DateTimeZone|null 21 | */ 22 | private $timezone; 23 | 24 | public function __construct(\DateTimeZone $timezone = null) 25 | { 26 | $this->timezone = $timezone; 27 | } 28 | 29 | public function serialize() 30 | { 31 | return \serialize($this->timezone); 32 | } 33 | 34 | public function unserialize($serialized) 35 | { 36 | $this->timezone = \unserialize($serialized); 37 | } 38 | 39 | public function getRegex(): string 40 | { 41 | return Patterns::REGEX_DATE; 42 | } 43 | 44 | /** 45 | * Convert passed argument to date in format YYYY-mm-dd 46 | * 47 | * @param int|string|\DateTimeInterface $rawData 48 | * 49 | * @return string 50 | * 51 | * @throws PatternException 52 | */ 53 | public function toUrl($rawData): string 54 | { 55 | $data = $this->processRawData($rawData); 56 | 57 | if (\is_object($data) && $data instanceof \DateTimeInterface) { 58 | return $data->format(static::DATE_FORMAT); 59 | } 60 | 61 | if (\is_int($data)) { 62 | return (new \DateTime())->setTimestamp($data)->format(static::DATE_FORMAT); 63 | } 64 | 65 | if (\is_string($data) && $this->match($data)) { 66 | $sData = (string) $data; 67 | if ($this->checkDate($sData)) { 68 | return $sData; 69 | } 70 | } 71 | 72 | throw $this->newInvalidToUrl($data); 73 | } 74 | 75 | private function processRawData($data) 76 | { 77 | if (\is_object($data)) { 78 | if ($data instanceof \DateTimeInterface) { 79 | return $data; 80 | } 81 | 82 | if (\method_exists($data, '__toString')) { 83 | $data = (string) $data; 84 | } 85 | } 86 | 87 | $d = Patterns::DELIMITER; 88 | if (\is_string($data) && \preg_match($d . '^(' . Patterns::REGEX_INT . ')$' . $d, $data)) { 89 | return (int) $data; 90 | } 91 | 92 | return $data; 93 | } 94 | 95 | /** 96 | * Convert date in format YYYY-mm-dd to \DateTimeImmutable object 97 | * 98 | * @param string $param 99 | * 100 | * @return \DateTimeImmutable 101 | * 102 | * @throws PatternException 103 | */ 104 | public function fromUrl(string $param) 105 | { 106 | if ($this->checkDate($param)) { 107 | return new \DateTimeImmutable($param, $this->timezone); 108 | } 109 | 110 | throw $this->newInvalidFromUrl($param); 111 | } 112 | 113 | private function checkDate(string &$input): bool 114 | { 115 | list($year, $month, $day) = \explode('-', $input); 116 | 117 | return \checkdate((int) $month, (int) $day, (int) $year); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/FloatPattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | use Awesomite\Chariot\Pattern\Patterns; 13 | 14 | class FloatPattern extends AbstractPattern 15 | { 16 | public function getRegex(): string 17 | { 18 | return Patterns::REGEX_FLOAT; 19 | } 20 | 21 | public function toUrl($data): string 22 | { 23 | $normalized = $this->normalizeInput($data); 24 | 25 | if ($this->isValidFloat($normalized)) { 26 | return $this->convertFloatToString($normalized); 27 | } 28 | 29 | if (\is_string($normalized) && $this->match($normalized)) { 30 | return $normalized; 31 | } 32 | 33 | throw $this->newInvalidToUrl($data); 34 | } 35 | 36 | public function fromUrl(string $param) 37 | { 38 | return (float) $param; 39 | } 40 | 41 | protected function isValidFloat($data): bool 42 | { 43 | return \is_float($data); 44 | } 45 | 46 | private function rtrimRedundantZeros(string $float): string 47 | { 48 | if (false !== \strpos($float, '.')) { 49 | return \rtrim(\rtrim($float, '0'), '.'); 50 | } 51 | 52 | return $float; 53 | } 54 | 55 | private function normalizeInput($input) 56 | { 57 | if (\is_string($input) || (\is_object($input) && \method_exists($input, '__toString'))) { 58 | return $this->rtrimRedundantZeros((string) $input); 59 | } 60 | 61 | if (\is_int($input) || \is_float($input)) { 62 | return (float) $input; 63 | } 64 | 65 | return $input; 66 | } 67 | 68 | private function convertFloatToString(float $input): string 69 | { 70 | return $this->rtrimRedundantZeros(\sprintf('%.20f', $input)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/IntPattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | use Awesomite\Chariot\Pattern\Patterns; 13 | 14 | class IntPattern extends AbstractPattern 15 | { 16 | public function getRegex(): string 17 | { 18 | return Patterns::REGEX_INT; 19 | } 20 | 21 | public function toUrl($data): string 22 | { 23 | if (\is_scalar($data) || (\is_object($data) && \method_exists($data, '__toString'))) { 24 | $result = (string) $data; 25 | if ($this->match($result)) { 26 | return $result; 27 | } 28 | } 29 | 30 | throw $this->newInvalidToUrl($data); 31 | } 32 | 33 | public function fromUrl(string $param) 34 | { 35 | return (int) $param; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/Ip4Pattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | use Awesomite\Chariot\Pattern\Patterns; 13 | 14 | class Ip4Pattern extends AbstractPattern 15 | { 16 | private static $maxIp4Int = 4294967295; 17 | 18 | private $options = FILTER_FLAG_IPV4; 19 | 20 | public function __construct(bool $allowPrivateRange = true, bool $allowReservedRange = true) 21 | { 22 | if (!$allowPrivateRange) { 23 | $this->options |= FILTER_FLAG_NO_PRIV_RANGE; 24 | } 25 | 26 | if (!$allowReservedRange) { 27 | $this->options |= FILTER_FLAG_NO_RES_RANGE; 28 | } 29 | } 30 | 31 | public function getRegex(): string 32 | { 33 | return Patterns::REGEX_IP; 34 | } 35 | 36 | public function toUrl($data): string 37 | { 38 | if (\is_object($data) && \method_exists($data, '__toString')) { 39 | $data = (string) $data; 40 | } 41 | 42 | if (\is_string($data)) { 43 | if ($this->match($data)) { 44 | return $data; 45 | } 46 | 47 | $d = Patterns::DELIMITER; 48 | if (\preg_match($d . '^(' . Patterns::REGEX_INT . ')$' . $d, $data)) { 49 | $data = (int) $data; 50 | } 51 | } 52 | 53 | if (\is_int($data)) { 54 | if ($data >= 0 && $data <= self::$maxIp4Int && $this->match($result = \long2ip($data))) { 55 | return $result; 56 | } 57 | } 58 | 59 | throw $this->newInvalidToUrl($data); 60 | } 61 | 62 | public function fromUrl(string $param) 63 | { 64 | if ($this->match($param)) { 65 | return $param; 66 | } 67 | 68 | throw $this->newInvalidFromUrl($param); 69 | } 70 | 71 | public function serialize() 72 | { 73 | return \serialize($this->options); 74 | } 75 | 76 | public function unserialize($serialized) 77 | { 78 | $this->options = \unserialize($serialized); 79 | } 80 | 81 | protected function match(string $data): bool 82 | { 83 | if (false === \filter_var($data, FILTER_VALIDATE_IP, $this->options)) { 84 | return false; 85 | } 86 | 87 | return parent::match($data); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/ListPattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | class ListPattern extends AbstractPattern 13 | { 14 | public function getRegex(): string 15 | { 16 | return '[^/]+(,[^/]+)*'; 17 | } 18 | 19 | public function toUrl($data): string 20 | { 21 | $normalized = $this->normalizeToUrl($data); 22 | 23 | if (\is_string($normalized)) { 24 | if (!$this->match($normalized)) { 25 | throw $this->newInvalidToUrl($data); 26 | } 27 | 28 | return $normalized; 29 | } 30 | 31 | if (!empty($normalized)) { 32 | $result = \implode(',', $normalized); 33 | if ($this->match($result)) { 34 | return $result; 35 | } 36 | } 37 | 38 | throw $this->newInvalidToUrl($data); 39 | } 40 | 41 | public function fromUrl(string $param) 42 | { 43 | return \explode(',', $param); 44 | } 45 | 46 | /** 47 | * @param $data 48 | * 49 | * @return array|string 50 | */ 51 | private function normalizeToUrl($data) 52 | { 53 | if (\is_object($data)) { 54 | if ($data instanceof \Traversable) { 55 | return \iterator_to_array($data); 56 | } 57 | 58 | if (\method_exists($data, '__toString')) { 59 | return (string) $data; 60 | } 61 | } 62 | 63 | if (\is_array($data)) { 64 | return $data; 65 | } 66 | 67 | if (\is_scalar($data) && !\is_bool($data)) { 68 | return (string) $data; 69 | } 70 | 71 | throw $this->newInvalidToUrl($data); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/RegexPattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | /** 13 | * @internal 14 | */ 15 | class RegexPattern extends AbstractPattern 16 | { 17 | protected $regex = ''; 18 | 19 | public function __construct(string $regex) 20 | { 21 | $this->regex = $regex; 22 | } 23 | 24 | public function getRegex(): string 25 | { 26 | return $this->regex; 27 | } 28 | 29 | public function toUrl($data): string 30 | { 31 | if ( 32 | !\is_scalar($data) 33 | && !\is_null($data) 34 | && !(\is_object($data) && \method_exists($data, '__toString')) 35 | ) { 36 | throw $this->newInvalidToUrl($data); 37 | } 38 | 39 | if (!$this->match((string) $data)) { 40 | throw $this->newInvalidToUrl($data); 41 | } 42 | 43 | return (string) $data; 44 | } 45 | 46 | public function fromUrl(string $param) 47 | { 48 | return $param; 49 | } 50 | 51 | public function serialize() 52 | { 53 | return $this->regex; 54 | } 55 | 56 | public function unserialize($serialized) 57 | { 58 | $this->regex = $serialized; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/UnsignedFloatPattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | use Awesomite\Chariot\Pattern\Patterns; 13 | 14 | class UnsignedFloatPattern extends FloatPattern 15 | { 16 | public function getRegex(): string 17 | { 18 | return Patterns::REGEX_UFLOAT; 19 | } 20 | 21 | protected function isValidFloat($data): bool 22 | { 23 | return parent::isValidFloat($data) && 0 <= $data; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Pattern/StdPatterns/UnsignedIntPattern.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Pattern\StdPatterns; 11 | 12 | use Awesomite\Chariot\Pattern\Patterns; 13 | 14 | class UnsignedIntPattern extends IntPattern 15 | { 16 | public function getRegex(): string 17 | { 18 | return Patterns::REGEX_UINT; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Reflections/Objects.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot\Reflections; 11 | 12 | /** 13 | * @internal 14 | */ 15 | class Objects 16 | { 17 | /** 18 | * Reads also private and protected properties 19 | * 20 | * @param $object 21 | * @param string $propertyName 22 | * 23 | * @return mixed 24 | */ 25 | public static function getProperty($object, string $propertyName) 26 | { 27 | $reflection = new \ReflectionObject($object); 28 | $property = $reflection->getProperty($propertyName); 29 | $property->setAccessible(true); 30 | 31 | return $property->getValue($object); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RouterInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | namespace Awesomite\Chariot; 11 | 12 | use Awesomite\Chariot\Exceptions\HttpException; 13 | 14 | interface RouterInterface 15 | { 16 | /** 17 | * @param string $method 18 | * @param string $path 19 | * 20 | * @return InternalRouteInterface 21 | * 22 | * @throws HttpException 23 | */ 24 | public function match(string $method, string $path): InternalRouteInterface; 25 | 26 | /** 27 | * @param string $url 28 | * 29 | * @return array e.g. ['GET', 'POST'] 30 | */ 31 | public function getAllowedMethods(string $url): array; 32 | 33 | /** 34 | * @param string $handler 35 | * @param string $method 36 | * 37 | * @return LinkInterface 38 | * 39 | * @throws HttpException 40 | */ 41 | public function linkTo(string $handler, string $method = HttpMethods::METHOD_ANY): LinkInterface; 42 | } 43 | --------------------------------------------------------------------------------