├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist └── src ├── Collector.php ├── Collectors ├── ControllerCollectorTrait.php └── ResourceCollectorTrait.php ├── Exceptions ├── BadRouteException.php ├── Http │ ├── BadRequestException.php │ ├── ConflictException.php │ ├── ForbiddenException.php │ ├── GoneException.php │ ├── HttpExceptionAbstract.php │ ├── LengthRequiredException.php │ ├── MethodNotAllowedException.php │ ├── NotAcceptableException.php │ ├── NotFoundException.php │ ├── PaymentRequiredException.php │ ├── PreconditionFailedException.php │ ├── RequestTimeOutException.php │ ├── ServiceUnavailableException.php │ ├── UnauthorizedException.php │ └── UnsupportedMediaTypeException.php └── MethodNotSupportedException.php ├── Group.php ├── Matcher.php ├── Parser.php ├── Path.php ├── Resource.php ├── Route.php └── Strategies ├── EnhancerAbstractStrategy.php ├── MatcherAwareInterface.php ├── RequestAwareTrait.php ├── RequestJsonStrategy.php ├── RequestResponseStrategy.php └── StrategyInterface.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 CodeBurner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codeburner Router 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/codeburner/router/v/stable)](https://packagist.org/packages/codeburner/router) 4 | [![Build Status](https://travis-ci.org/codeburnerframework/router.svg?branch=master)](https://travis-ci.org/codeburnerframework/router) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/codeburnerframework/routing/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/codeburnerframework/routing/?branch=master) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/codeburnerframework/routing/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/codeburnerframework/routing/?branch=master) 7 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 8 | 9 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d96c4a67-982b-4e16-a24d-7b490bf11bc7/big.png)](https://insight.sensiolabs.com/projects/d96c4a67-982b-4e16-a24d-7b490bf11bc7) 10 | 11 | An blazing fast PHP router system with amazing features and abstraction. 12 | 13 | Thank's to [Nikita Popov's](https://github.com/nikic/) for motivate me with [this post](https://nikic.github.io/2014/02/18/Fast-request-Router-using-regular-expressions.html). 14 | 15 | ## Installation 16 | 17 | Add `codeburner/router` to your `composer.json` file, and update or install composer dependencies. 18 | 19 | ```json 20 | { 21 | "require": { 22 | "codeburner/router": "^2.0" 23 | } 24 | } 25 | ``` 26 | 27 | or via CLI: 28 | 29 | ```bash 30 | $ composer require codeburner/router --save 31 | ``` 32 | 33 | ## Table of Content 34 | 35 | - [Introduction](#introduction) 36 | - [Performance](#performance) 37 | - [Concepts](#concepts) 38 | - [Usage](#usage) 39 | - [Routes](#routes) 40 | - [Patterns](#patterns) 41 | - [Constraints](#constraints) 42 | - [Wildcards](#wildcards) 43 | - [Optional Segments](#optional-segments) 44 | - [Actions](#actions) 45 | - [Strategies](#strategies) 46 | - [Enhancers](#enhancers) 47 | - [PSR7](#psr7) 48 | - [Default Arguments](#default-arguments) 49 | - [Container Integration](#container-integration) 50 | - [Names](#names) 51 | - [Metadata](#metadata) 52 | - [Collector](#collector) 53 | - [Groups](#groups) 54 | - [Resources](#resources) 55 | - [Restricting Actions](#restricting-actions) 56 | - [Prefixing Resources](#prefixing-resources) 57 | - [Ignoring Resource Name](#ignoring-resource-name) 58 | - [Nested Resources](#nested-resources) 59 | - [Nesting Limit](#nesting-limit) 60 | - [Shallow Resources](#shallow-resources) 61 | - [Adding More Actions](#adding-more-actions) 62 | - [Resources Route Names](#resources-route-names) 63 | - [Translated Patterns](#translated-patterns) 64 | - [Controllers](#controllers) 65 | - [Annotated Definition](#annotated-definition) 66 | - [Prefixing Controllers](#prefixing-controllers) 67 | - [Ignoring Controller Name](#ignoring-controller-name) 68 | - [Changing Pattern Join](#changing-pattern-join) 69 | - [Matcher](#matcher) 70 | - [Exceptions](#exceptions) 71 | - [Method not Allowed](#method-not-allowed) 72 | - [List of Exceptions](#list-of-exceptions) 73 | - [Basepath](#basepath) 74 | 75 | ## Introduction 76 | 77 | Welcome to the fastest PHP router system docs! Before starting the usage is recommended understand the main goal and mission of all parts of this package. 78 | 79 | ### Performance 80 | 81 | Codeburner project create packages with performance in focus, the Codeburner Router was compared with [Nikic's fast route](https://github.com/nikic/fastroute) a fast and base package for several route systems, including the [Laravel](http://laravel.com) and [SlimFramework](http://slimframework.com). 82 | 83 | The Tests reveals that Codeburner Router can be in average **70% faster** while give a full abstraction level in handling routes. More details about the benchmark including the comparisons using [blackfire](http://blackfire.io) of [scripts that maps 100 routes with several arguments and execute them](https://gist.github.com/alexrohleder96/bc302708653b68d1b053), can be found [here](https://blackfire.io/profiles/compare/b861281b-b6d4-4015-b25b-fd9399e789ba...914e81ac-0898-4633-81b1-6c9f1bbfab69/graph?settings%5Bdimension%5D=wt&settings%5Bdisplay%5D=landscape&settings%5BtabPane%5D=nodes&selected=&callname=main()). 84 | 85 | 86 | ### Concepts 87 | 88 | The router recognize requests and maps to a logic, an action. For example, when the application receive an incoming request for: 89 | 90 | ```php 91 | "GET" "/article/17" 92 | ``` 93 | 94 | It asks the router to match it to a action, if the first matching route is: 95 | 96 | ```php 97 | $collector->get("/article/{id}", "ArticleResource::show"); 98 | ``` 99 | 100 | The request is dispatched to the `ArticleResource`'s `show` method with `17` as parameter. 101 | 102 | 103 | ## Usage 104 | 105 | After a successful installation via composer, or a manual including of `Collector.php` and `Matcher.php` you can begin using the package. 106 | 107 | ```php 108 | include "vendor/autoload.php"; 109 | 110 | use Codeburner\Router\Collector; 111 | use Codeburner\Router\Matcher; 112 | 113 | $collector = new Collector(); 114 | $matcher = new Matcher($collector); 115 | 116 | $collector->get("/", function () { 117 | echo "Hello World!"; 118 | }); 119 | 120 | $route = $matcher->match("get", "/"); 121 | $route->call(); 122 | ``` 123 | 124 | More examples can be found [here](https://www.github.com/codeburnerframework/router/tree/examples). 125 | 126 | 127 | ## Routes 128 | 129 | After the release of v2.0.0 all routes are objects, and can be handled in groups. All route attributes can be modified at run time, and you can store a route created in the `Codeburner\Router\Collector` in a var. Every time you create a route by any `Codeburner\Router\Collector` method, a new `Route` object is created and by default a `Group` is returned containing these route. 130 | 131 | 132 | ### Patterns 133 | 134 | Patterns are representation of request paths, it follows the popular definitions created by [FastRoute](https://github.com/nikic/FastRoute), if you are familiar with [Laravel](laravel.com) or [Slim Framework](slimframework.com) you will not have problems here. 135 | 136 | If you not, all you need to know for now, is that dynamic segments in patterns, or vars or even parameters if you prefer, are defined inside `{` and `}`. All parts of pattern inside this will be captured by the matcher and passed to the action. 137 | 138 | 139 | #### Constraints 140 | 141 | Routes can define dynamic pattern segments, and that can have a constraint of match, in other words you could define that these segment must be an `int` or a `uid`. The constraint definition follows the pattern adopted by most of routers, for example, to enforce the format of slugs the constraint must be something that have letters, numbers and hyphens, not more. 142 | 143 | ```php 144 | "/articles/{article:[a-z0-9-]+}" 145 | ``` 146 | 147 | > **NOTE:** As a constraint is essentially a portion of a bigger REGEX there is a restriction of use of capturing groups. For example `{lang:(en|de)}` is not a valid placeholder, because `()` is a capturing group. Instead you can use either `{lang:en|de}` or `{lang:(?:en|de)}`. 148 | 149 | 150 | ##### Wildcards 151 | 152 | THe collector came with support to wildcards in place of regexes in constraints, you can define your own wildcards with the `setWildcard(string name, string regex)` collector's method. There are 8 wildcards with more 3 aliases in a total of 11 wildcards, that are listed bellow: 153 | 154 | - uid: `uid-[a-zA-Z0-9]` 155 | - slug: `[a-z0-9-]` 156 | - string: `\w` 157 | - int or integer: `\d` 158 | - float or double: `[-+]?\d*?[.]?\d` 159 | - hex: `0[xX][0-9a-fA-F]` 160 | - octal: `0[1-7][0-7]` 161 | - bool or boolean: `1|0|true|false|yes|no` 162 | 163 | > **NOTE:** All the build-in **wildcards came with no quantifier**, but support [quantifiers](https://msdn.microsoft.com/en-us/library/3206d374.aspx) after they use, it's not a rule. 164 | 165 | 166 | #### Optional Segments 167 | 168 | You can define several patterns at once, with optional segments that can be nested. For optinal segments in your routes use the `[` and `]` statement to embrace the optional part. Optional segments must only be in the end of pattern and close all opened `[` with `]`. For example: 169 | 170 | ```php 171 | "/user/photos[/{id:uid}]" 172 | ``` 173 | 174 | 175 | ### Actions 176 | 177 | Routes define what request must execute what action. An action support all the definitions ways of a [callables](http://php.net/manual/language.types.callable.php) of PHP. 178 | 179 | All the parameters defined on the route pattern are accessible to be used on a dynamic action definition. All parameters will be snake-cased, and words separator are identified by a "-" character. In the example bellow if we request `/photos/get/30` the `PhotosResource`'s `getLimited` method will be called with `30` as parameter. 180 | 181 | ```php 182 | "/{resource:string+}/get/{count:int+}" "{resource}Controller::getLimited" 183 | ``` 184 | 185 | 186 | #### Strategies 187 | 188 | A route must be able to execute the action. By default the action is executed by a simple `call_user_func_array` call, but you can define a more specific way to do that individually for each route or group with the `setStrategy` method, so each route can have different behaviors. 189 | 190 | To define a new strategy simply create a class that implements the `Codeburner\Router\Strategies\StrategyInterface` interface and the `call` method that receives the `Codeburner\Router\Route` matched as unique parameter. A strategy can receive the `Codeburner\Router\Matcher` using the interface `Codeburner\Router\Strategy\MatcherAwareInterface`. 191 | 192 | 193 | ##### Enhancers 194 | 195 | The route enhancer strategy act like a bridge between one route and it dispatch strategy. In this "bridge" operations are made manipulating the route object to adapt it, it's common to use the [metadata information](#metadata) in this place. 196 | 197 | The real strategy is defined in the route metadata, using the key `strategy`, after the execution of enhancement logic the real strategy will be called. 198 | 199 | To create one enhancer you only need to extends the `Codeburner\Router\Strategies\EnhancerAbstractStrategy`. 200 | 201 | 202 | ##### PSR7 203 | 204 | Codeburner have support to [psr7 objects](http://www.php-fig.org/psr/psr-7/), there are at this version two strategies that give your actions a `request` and `response` objects and handle generated `response` objects. Both `Codeburner\Router\Strategies\RequestResponseStrategy` and `Codeburner\Router\Strategies\RequestJsonStrategy` receive one `Psr\Http\Message\RequestInterface` and one `Psr\Http\Message\ResponseInterface`, and make the `matcher`'s `call` method return a `Psr\Http\Message\ResponseInterface`. 205 | 206 | ```php 207 | $collector->get("/article/{id:int{5}}", function (RequestInterface $request, ResponseInterface $response, array $args) { 208 | return (string) getCommentById($args["id"]); 209 | })->setStrategy(new RequestResponseStrategy($request, $response)); 210 | ``` 211 | 212 | ```php 213 | $collector->get("/article/{id:int{5}}", function (RequestInterface $request, array $args) { 214 | return (array) getCommentById($args["id"]); 215 | })->setStrategy(new RequestJsonStrategy($request, $response)); 216 | ``` 217 | 218 | Instead of creating strategy objects by yourself, you could use a [container wrapper](#container-integration) on `call` method. 219 | 220 | ```php 221 | $route = $collector->get("/{id:int{5}}", function (RequestInterface $request, ResponseInterface $response, array $args) { 222 | return $args["id"]; 223 | }); 224 | 225 | $route->call(function ($class) use ($container) { 226 | return $container->get($class); 227 | }); 228 | ``` 229 | 230 | 231 | #### Default Arguments 232 | 233 | If is necessary to define arguments that are not found on the pattern, use the `setDefault(string key, mixed value)` method. These arguments will be merged with the given from the pattern. 234 | 235 | ```php 236 | $collector->get("/", function ($arg) { 237 | echo $arg; 238 | })->setDefault("arg", "hello world"); 239 | ``` 240 | 241 | 242 | #### Container Integration 243 | 244 | If is necessary to inject dependencies on controllers, resources, or even strategies, you can tell the `call` method on `Route` objects to use a closure that receives the class name and return one instance of these class. 245 | 246 | ```php 247 | $route->call(function ($class) use ($container) { 248 | return $container->get($class); 249 | }); 250 | ``` 251 | 252 | 253 | ### Names 254 | 255 | All the routes allow you to apply names to then, this names can be used to find a route in the `Collector` or to generate links with `Path`'s method `to(string name, array args = [])`. E.g. 256 | 257 | ```php 258 | // ... 259 | // The Path class will create several links for us, just give they new object a instance of the collector. 260 | $path = new Codeburner\Router\Path($collector); 261 | // ... 262 | // Setting the name of route to blog.article 263 | $collector->get("/blog/{article:slug+}", "blog::show")->setName("blog.article"); 264 | // ... 265 | // this will print an anchor tag with "/blog/my-first-article" in href. 266 | echo "My First Article"; 267 | ``` 268 | 269 | > **NOTE:** For best practice use the dot for delimiting namespaces in your route names, so you can group and find they names easily. The [resource](#resources-route-names) collector adopt this concept. 270 | 271 | 272 | ### Metadata 273 | 274 | Sometimes you want to delegate more information to a route, for post match filters or action execution strategies. For persist data that will not be passed to action but used in somewhere before the execution use the `setMetadata(string key, mixed value)` method. 275 | 276 | For getting the metadata use the `Codeburner\Router\Route`'s `getMetadataArray()` method for getting all of each, and `getMetadata(string key)` to get a specific metadata, you can check if a metadata exists with `hasMetadata(string key)` method. 277 | 278 | 279 | ## Collector 280 | 281 | The collector hold all routes and give to the matcher, and more important than that, implements all the abstraction layer of defining routes. 282 | 283 | 284 | ### Groups 285 | 286 | All routes returned by the collector are `Codeburner\Router\Group` instances, even if it's a single route. With these groups you can use most of the `Codeburner\Router\Route` methods but applying the changes to all routes in the group. You can create groups with the `Codeburner\Router\Collector`'s `group` method that receive an array of routes or instantiating a new instance of `Codeburner\Router\Group` and add all routes with `set` method. 287 | 288 | 289 | ### Resources 290 | 291 | Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index, show, make, edit, create, update and destroy actions, a resourceful route declares them in a single line of code. 292 | 293 | ```php 294 | $collector->resource('PhotosResource'); 295 | ``` 296 | 297 | The collector will create seven new routes for `PhotosResource`, as listed bellow: 298 | 299 | Method |Path |Controller::Action |Used For 300 | ---------|-------------------|---------------------------|--------------------------------------------- 301 | GET | /photos | PhotosResource::index | Display a list of all photos 302 | GET | /photos/make | PhotosResource::make | Return an HTML form for creating a new photo 303 | POST | /photos | PhotosResource::create | Create a new photo 304 | GET | /photos/{id} | PhotosResource::show | Display a specific photo 305 | GET | /photos/{id}/edit | PhotosResource::edit | Return an HTML form for editing a photo 306 | PUT | /photos/{id} | PhotosResource::update | Update a specific photo 307 | DELETE | /photos/{id} | PhotosResource::destroy | Delete a specific photo 308 | 309 | > **NOTE:** Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to seven different actions. 310 | 311 | 312 | #### Restricting Actions 313 | 314 | There is two ways to define what of the seven resource routes should be created, with the `only` or `except` as option in `resource(string resource, array options = null)` method, 315 | 316 | ```php 317 | // create only the index and show routes. 318 | $collector->resource("ArticleResource", ["only" => ["index", "show"]]); 319 | 320 | // create only the index and show routes too, because all the others should not be created. 321 | $collector->resource("ArticleResource", ["except" => ["make", "create", "destroy", "update", "edit"]]); 322 | ``` 323 | 324 | or with the `only` and `except` methods of `Codeburner\Router\Resource` object returned by the `resource(string resource, array options = null)` method. 325 | 326 | ```php 327 | // create only the index and show routes. 328 | $collector->resource("ArticleResource")->only(["index", "show"]); 329 | 330 | // create only the index and show routes too, because all the others should not be created. 331 | $collector->resource("ArticleResource")->except["make", "create", "destroy", "update", "edit"]); 332 | ``` 333 | 334 | 335 | #### Prefixing Resources 336 | 337 | By default all resource patterns receive the resource name as prefix, on previous example the `UserResource` generate a `/user` prefix. To alter this pass an array with `as` option to the `resource(string resource, array option = null)` method, these option will be used as prefix. eg. 338 | 339 | ```php 340 | // now the pattern for make action will be /account/make 341 | $collector->resource("UserResource", ["as" => "account"]); 342 | ``` 343 | 344 | 345 | ##### Ignoring Resource Name 346 | 347 | You can avoid this by using the `resourceWithoutPrefix(string resource)` instead of `resource(string resource, array option = null)` method, the same way for multiple matching methods `resources(string[] resource)` and `resourcesWithoutPrefix(string[] resources)`. 348 | 349 | 350 | #### Nested Resources 351 | 352 | It's common to have resources that are logically children of other resources. For example one `article` always have one `category`. Nested routes allow you to capture this relationship in your routing. In this case, you could include this route declaration: 353 | 354 | ```php 355 | $collector->resource("CategoryResource")->nest( 356 | $collector->resource("ArticleResource") 357 | ); 358 | ``` 359 | 360 | In addition to the routes for `CategoryResource`, this declaration will also route to `ArticleResource` with one category as parameter. 361 | 362 | Method |Path |Controller::Action |Used For 363 | ---------|----------------------------------------------|----------------------------|----------------------------------------------- 364 | GET | /category/{category_id}/article | ArticleResource::index | Display a list of all Article 365 | GET | /category/{category_id}/article/make | ArticleResource::make | Return an HTML form for creating a new article 366 | POST | /category/{category_id}/article | ArticleResource::create | Create a new article 367 | GET | /category/{category_id}/article/{id} | ArticleResource::show | Display a specific article 368 | GET | /category/{category_id}/article/{id}/edit | ArticleResource::edit | Return an HTML form for editing a article 369 | PUT | /category/{category_id}/article/{id} | ArticleResource::update | Update a specific article 370 | DELETE | /category/{category_id}/article/{id} | ArticleResource::destroy | Delete a specific article 371 | 372 | ##### Nesting Limit 373 | 374 | You can nest resources within other nested resources if you like. For example: 375 | 376 | ```php 377 | $collector->resource("CategoryResource")->nest( 378 | $collector->resource("ArticleResource")->nest( 379 | $collector->resource("CommentResource") 380 | ) 381 | ); 382 | ``` 383 | Deeply-nested resources quickly become cumbersome. In this case, for example, the application would recognize paths such as: 384 | 385 | ```php 386 | "/category/1/article/2/comment/3" 387 | ``` 388 | 389 | > **TIP:** Resources should never be nested more than 1 level deep. 390 | 391 | ###### Shallow Resources 392 | 393 | One way to avoid deep nesting (as recommended above) is to generate the collection actions scoped under the parent, so as to get a sense of the hierarchy, but to not nest the member actions. In other words, to only build routes with the minimal amount of information to uniquely identify the resource, like this: 394 | 395 | ```php 396 | $collector->resource("ArticleResource")->nest( 397 | $collector->resource("CommentResource")->only(["index", "make", "create"]); 398 | ); 399 | 400 | $collector->resource("CommentResource")->except(["index", "make", "create"]); 401 | ``` 402 | 403 | This idea strikes a balance between descriptive routes and deep nesting. There exists shorthand syntax to achieve just that, via the `shallow` method in `Codeburner\Router\Resource`: 404 | 405 | ```php 406 | $collector->resource("ArticleResource")->shallow( 407 | $collector->resource("CommentResource") 408 | ); 409 | ``` 410 | 411 | This will generate the exact same routes as the first example. 412 | 413 | > **NOTE:** `shallow` method act the same way as `nest` method, so you can always nest these methods, and use one with each other. 414 | 415 | ##### Adding More Actions 416 | 417 | You are not limited to the seven routes that RESTFul routing creates by default. If you like, you may add additional routes that apply to the `Codeburner\Router\Resource`. The example above will create an additional route with `/photos/{id}/preview` pattern in `get` method. 418 | 419 | ```php 420 | $collector->resource("PhotosResource")->member( 421 | $collector->get("/preview", "PhotosResource::preview") 422 | ); 423 | ``` 424 | 425 | 426 | #### Resources Route Names 427 | 428 | All the routes in resource receive a [name](#names) that will be composed by the resource name or prefix, a dot and the action name. e.g. 429 | 430 | ```php 431 | class PhotosResource { 432 | public function index() { 433 | 434 | } 435 | } 436 | 437 | $collector->resource("PhotosResource")->only("index"); 438 | $collector->resource("PhotosResource", ["as" => "picture"])->only("index"); 439 | 440 | echo $path->to("photos.index"), "
", $path->to("picture.index"); 441 | ``` 442 | 443 | 444 | #### Translated Patterns 445 | 446 | If you prefer to translate the patterns generated by the resource, just define an `translate` option that receives an array with one or the two keys, `new` and `edit`. 447 | 448 | ```php 449 | $collector->resource("ArticleResource", ["as" => "kategorien", "translate" => ["new" => "neu", "edit": "bearbeiten"]); 450 | ``` 451 | 452 | Or using the `translate(array translations)` method of `Codeburner\Router\Resource`. 453 | 454 | ```php 455 | $collector->resource("ArticleResource", ["as" => "kategorien"])->translate(["new" => "neu", "edit": "bearbeiten"]); 456 | ``` 457 | 458 | The two examples above translate `ArticleResource` routes to german, changing the prefix to `kategorien` and the `new` and `edit` keywords to `neu` and `bearbeiten` respectively. 459 | 460 | 461 | ### Controllers 462 | 463 | Controllers can be fully mapped by the `Codeburner\Router\Collector`, avoiding the manually description of routes to controller actions. To reach this abstraction some definitions must be respected: 464 | 465 | - Methods that can be matched **must** begin with the corresponding HTTP method, like `get`, `post`, `put`, `patch` and `delete`. 466 | - Camelcased method name will be converted to pattern, each word by default will receive `/` by prefix. 467 | 468 | ```php 469 | class UserController 470 | { 471 | public function getName() 472 | { 473 | // the same as $collector->get("/user/name", "UserController::getName") 474 | } 475 | } 476 | ``` 477 | 478 | 479 | #### Annotated Information 480 | 481 | All the [PHPDoc @param](http://www.phpdoc.org/docs/latest/references/phpdoc/tags/param.html) are parsed and the methods arguments receive a [constraint](#constraints). All the [wildcards](#wildcards) are allowed here, and you can set the type of argument as an [constraint](#constraints) too. 482 | 483 | A new annotation is available for defining [strategies](#strategies) to specific methods. For this use the `@strategy` annotation. 484 | 485 | ```php 486 | class BlogController 487 | { 488 | /** 489 | * @param int $id 490 | * @annotation MyActionExcecutorStrategy 491 | */ 492 | public function getPost($id) 493 | { 494 | // the same as $collector->get("/blog/post/{id:int+}", "BlogController::getPost") 495 | // ->setStrategy("MyActionExecutorStrategy") 496 | } 497 | } 498 | ``` 499 | 500 | 501 | #### Prefixing Controllers 502 | 503 | Act the same way of [prefixing resources](#prefixing-resources), passing the option `as` to `controller(string controller, array options = null)` method. 504 | 505 | 506 | ##### Ignoring Controller Name 507 | 508 | Same way of [ignoring resource name](#ignoring-resource-name), use the `controllerWithoutPrefix(string controller)` method, or the `controllersWithoutPrefix(string[] controllers)` method. 509 | 510 | 511 | #### Changing Pattern Join 512 | 513 | If you wanna change the default pattern joiner `/` by another join like `-`, you only need to define that before the call of `Codeburner\Router\Collector`'s `controller` method. 514 | 515 | In the example bellow the pattern constructed by the `getName` method of `UserController` will be `/user-name` instead of `/user/name`. 516 | 517 | ```php 518 | $collector->setControllerActionJoin("-"); 519 | $collector->controller("UserController"); 520 | ``` 521 | 522 | 523 | ## Matcher 524 | 525 | The matcher is responsible for determining which route should be executed for a given request information. 526 | 527 | 528 | ### Basepath 529 | 530 | An important point of matcher is that it can remove the basepath prefix from the routes patterns, for this the first parameter of the matcher constructor should be a string with the basepath. 531 | 532 | So if you want to declare routes for a blog system living in `https://www.yourdomain.com/blog` create a new matcher that ignore the `/blog`, so all you declarations can skip this segment. 533 | 534 | 535 | ### Exceptions 536 | 537 | There are several exceptions for HTTP errors provided by the codeburner router system, but there are special exceptions, that have methods for determining the logic of failure. 538 | 539 | 540 | #### Method not Allowed 541 | Route method is wrong `Codeburner\Router\Exceptions\Http\MethodNotAllowedException` 542 | 543 | ```php 544 | $collector->get("/foo", "controller::action"); 545 | 546 | try { 547 | $matcher->match("post", "/foo"); 548 | } catch (Codeburner\Router\Exceptions\MethodNotAllowedException $e) { 549 | // You can for example, redirect to the correct request. 550 | // this if verify if the requested route can serve get requests. 551 | if ($e->can("get")) { 552 | // if so, dispatch into get method. 553 | $matcher->match("get", $e->requested_uri); 554 | } 555 | } 556 | ``` 557 | 558 | #### List of Exceptions 559 | 560 | - Codeburner\Router\Exceptions\BadRouteException 561 | - Codeburner\Router\Exceptions\MethodNotSupportedException 562 | - Codeburner\Router\Exceptions\Http\HttpExceptionAbstract 563 | - Codeburner\Router\Exceptions\Http\BadRequestException 564 | - Codeburner\Router\Exceptions\Http\ConflictException 565 | - Codeburner\Router\Exceptions\Http\ForbiddenException 566 | - Codeburner\Router\Exceptions\Http\GoneException 567 | - Codeburner\Router\Exceptions\Http\LengthRequiredException 568 | - Codeburner\Router\Exceptions\Http\MethodNotAllowedException 569 | - Codeburner\Router\Exceptions\Http\NotAcceptableException 570 | - Codeburner\Router\Exceptions\Http\NotFoundException 571 | - Codeburner\Router\Exceptions\Http\PaymentRequiredException 572 | - Codeburner\Router\Exceptions\Http\PreconditionFailedException 573 | - Codeburner\Router\Exceptions\Http\RequestTimeOutException 574 | - Codeburner\Router\Exceptions\Http\ServiceUnavailableException 575 | - Codeburner\Router\Exceptions\Http\UnauthorizedException 576 | - Codeburner\Router\Exceptions\Http\UnsupportedMediaTypeException 577 | 578 | > **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the 579 | `Allow:` header to detail available methods for the requested resource. For this you can get a 580 | string with a processed allowed methods by using the `allowed` method of this exception. 581 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeburner/router", 3 | "description": "The amazing Codeburner framework Router system.", 4 | "keywords": ["router", "dispatcher", "codeburner"], 5 | "license": "MIT", 6 | "type": "library", 7 | "authors": [ 8 | { 9 | "name": "Alex Rohleder", 10 | "email": "alexrohleder96@outlook.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.5.0", 15 | "psr/http-message": "~1.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~4.0", 19 | "zendframework/zend-diactoros": "^1.3" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Codeburner\\Router\\": "src" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests/ 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Collector.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router; 12 | 13 | use Codeburner\Router\Exceptions\BadRouteException; 14 | use Codeburner\Router\Exceptions\MethodNotSupportedException; 15 | 16 | /** 17 | * Explicit Avoiding autoload for classes and traits 18 | * that are aways needed. Don't need an condition of class exists 19 | * because the routes will not be used until the collector is used. 20 | */ 21 | 22 | if (!class_exists(Parser::class, false)) { 23 | include __DIR__ . "/Parser.php"; 24 | } 25 | 26 | include __DIR__ . "/Route.php"; 27 | include __DIR__ . "/Group.php"; 28 | include __DIR__ . "/Collectors/ControllerCollectorTrait.php"; 29 | include __DIR__ . "/Collectors/ResourceCollectorTrait.php"; 30 | 31 | /** 32 | * The Collector class hold, parse and build routes. 33 | * 34 | * @author Alex Rohleder 35 | */ 36 | 37 | class Collector 38 | { 39 | 40 | use Collectors\ControllerCollectorTrait; 41 | use Collectors\ResourceCollectorTrait; 42 | 43 | 44 | /** 45 | * All the supported http methods separated by spaces. 46 | * 47 | * @var string 48 | */ 49 | 50 | const HTTP_METHODS = "get post put patch delete"; 51 | 52 | /** 53 | * The static routes are simple stored in a multidimensional array, the first 54 | * dimension is indexed by an http method and hold an array indexed with the patterns 55 | * and holding the route. ex. [METHOD => [PATTERN => ROUTE]] 56 | * 57 | * @var Route[][] 58 | */ 59 | 60 | protected $statics = []; 61 | 62 | /** 63 | * The dynamic routes have parameters and are stored in a hashtable that every cell have 64 | * an array with route patterns as indexes and routes as values. ex. [INDEX => [PATTERN => ROUTE]] 65 | * 66 | * @var Route[][] 67 | */ 68 | 69 | protected $dynamics = []; 70 | 71 | /** 72 | * @var Route[] 73 | */ 74 | 75 | protected $named = []; 76 | 77 | /** 78 | * The pattern parser instance. 79 | * 80 | * @var Parser 81 | */ 82 | 83 | protected $parser; 84 | 85 | /** 86 | * Collector constructor. 87 | * 88 | * @param Parser|null $parser 89 | */ 90 | 91 | public function __construct(Parser $parser = null) 92 | { 93 | $this->parser = $parser ?: new Parser; 94 | } 95 | 96 | /** 97 | * @param string $method 98 | * @param string $pattern 99 | * @param callable $action 100 | * 101 | * @throws BadRouteException 102 | * @throws MethodNotSupportedException 103 | * 104 | * @return Group 105 | */ 106 | 107 | public function set($method, $pattern, $action) 108 | { 109 | $method = $this->getValidMethod($method); 110 | $patterns = $this->parser->parsePattern($pattern); 111 | $group = new Group; 112 | 113 | foreach ($patterns as $pattern) 114 | { 115 | $route = new Route($this, $method, $pattern, $action); 116 | $group->setRoute($route); 117 | 118 | if (strpos($pattern, "{") !== false) { 119 | $index = $this->getDynamicIndex($method, $pattern); 120 | $this->dynamics[$index][$pattern] = $route; 121 | } else $this->statics[$method][$pattern] = $route; 122 | } 123 | 124 | return $group; 125 | } 126 | 127 | public function get ($pattern, $action) { return $this->set("get" , $pattern, $action); } 128 | public function post ($pattern, $action) { return $this->set("post" , $pattern, $action); } 129 | public function put ($pattern, $action) { return $this->set("put" , $pattern, $action); } 130 | public function patch ($pattern, $action) { return $this->set("patch" , $pattern, $action); } 131 | public function delete($pattern, $action) { return $this->set("delete", $pattern, $action); } 132 | 133 | /** 134 | * Insert a route into several http methods. 135 | * 136 | * @param string[] $methods 137 | * @param string $pattern 138 | * @param callable $action 139 | * 140 | * @return Group 141 | */ 142 | 143 | public function match(array $methods, $pattern, $action) 144 | { 145 | $group = new Group; 146 | foreach ($methods as $method) 147 | $group->set($this->set($method, $pattern, $action)); 148 | return $group; 149 | } 150 | 151 | /** 152 | * Insert a route into every http method supported. 153 | * 154 | * @param string $pattern 155 | * @param callable $action 156 | * 157 | * @return Group 158 | */ 159 | 160 | public function any($pattern, $action) 161 | { 162 | return $this->match(explode(" ", self::HTTP_METHODS), $pattern, $action); 163 | } 164 | 165 | /** 166 | * Insert a route into every http method supported but the given ones. 167 | * 168 | * @param string $methods 169 | * @param string $pattern 170 | * @param callable $action 171 | * 172 | * @return Group 173 | */ 174 | 175 | public function except($methods, $pattern, $action) 176 | { 177 | return $this->match(array_diff(explode(" ", self::HTTP_METHODS), (array) $methods), $pattern, $action); 178 | } 179 | 180 | /** 181 | * Group all given routes. 182 | * 183 | * @param Route[] $routes 184 | * @return Group 185 | */ 186 | 187 | public function group(array $routes) 188 | { 189 | $group = new Group; 190 | foreach ($routes as $route) 191 | $group->set($route); 192 | return $group; 193 | } 194 | 195 | /** 196 | * Remove a route from collector. 197 | * 198 | * @param string $method 199 | * @param string $pattern 200 | */ 201 | 202 | public function forget($method, $pattern) 203 | { 204 | if (strpos($pattern, "{") === false) { 205 | unset($this->statics[$method][$pattern]); 206 | } else unset($this->dynamics[$this->getDynamicIndex($method, $pattern)][$pattern]); 207 | } 208 | 209 | /** 210 | * @param string $name 211 | * @return Route|false 212 | */ 213 | 214 | public function findNamedRoute($name) 215 | { 216 | if (!isset($this->named[$name])) { 217 | return false; 218 | } else return $this->named[$name]; 219 | } 220 | 221 | /** 222 | * @param string $method 223 | * @param string $pattern 224 | * 225 | * @return Route|false 226 | */ 227 | 228 | public function findStaticRoute($method, $pattern) 229 | { 230 | $method = strtolower($method); 231 | if (isset($this->statics[$method]) && isset($this->statics[$method][$pattern])) 232 | return $this->statics[$method][$pattern]; 233 | return false; 234 | } 235 | 236 | /** 237 | * @param string $method 238 | * @param string $pattern 239 | * 240 | * @return array|false 241 | */ 242 | 243 | public function findDynamicRoutes($method, $pattern) 244 | { 245 | $index = $this->getDynamicIndex($method, $pattern); 246 | return isset($this->dynamics[$index]) ? $this->dynamics[$index] : false; 247 | } 248 | 249 | /** 250 | * @param string $method 251 | * @param string $pattern 252 | * 253 | * @return int 254 | */ 255 | 256 | protected function getDynamicIndex($method, $pattern) 257 | { 258 | return crc32(strtolower($method)) + substr_count($pattern, "/"); 259 | } 260 | 261 | /** 262 | * Determine if the http method is valid. 263 | * 264 | * @param string $method 265 | * 266 | * @throws MethodNotSupportedException 267 | * @return string 268 | */ 269 | 270 | protected function getValidMethod($method) 271 | { 272 | $method = strtolower($method); 273 | 274 | if (strpos(self::HTTP_METHODS, $method) === false) { 275 | throw new MethodNotSupportedException($method); 276 | } 277 | 278 | return $method; 279 | } 280 | 281 | /** 282 | * @param string $name 283 | * @param Route $route 284 | * 285 | * @return self 286 | */ 287 | 288 | public function setRouteName($name, Route $route) 289 | { 290 | $this->named[$name] = $route; 291 | return $this; 292 | } 293 | 294 | /** 295 | * @return string[] 296 | */ 297 | 298 | public function getWildcards() 299 | { 300 | return $this->parser->getWildcards(); 301 | } 302 | 303 | /** 304 | * @return string[] 305 | */ 306 | 307 | public function getWildcardTokens() 308 | { 309 | return $this->parser->getWildcardTokens(); 310 | } 311 | 312 | /** 313 | * @param string $wildcard 314 | * @return string|null 315 | */ 316 | 317 | public function getWildcard($wildcard) 318 | { 319 | return $this->parser->getWildcard($wildcard); 320 | } 321 | 322 | /** 323 | * @param string $wildcard 324 | * @param string $pattern 325 | * 326 | * @return self 327 | */ 328 | 329 | public function setWildcard($wildcard, $pattern) 330 | { 331 | $this->parser->setWildcard($wildcard, $pattern); 332 | return $this; 333 | } 334 | 335 | /** 336 | * @return Parser 337 | */ 338 | 339 | public function getParser() 340 | { 341 | return $this->parser; 342 | } 343 | 344 | /** 345 | * @param Parser $parser 346 | * 347 | * @throws \LogicException 348 | * @return self 349 | */ 350 | 351 | public function setParser(Parser $parser) 352 | { 353 | if (!empty($this->statics) || !empty($this->dynamics)) { 354 | throw new \LogicException("You can't define a route parser after registering a route."); 355 | } 356 | 357 | $this->parser = $parser; 358 | return $this; 359 | } 360 | 361 | } 362 | -------------------------------------------------------------------------------- /src/Collectors/ControllerCollectorTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Collectors; 12 | 13 | use Codeburner\Router\Collector; 14 | use Codeburner\Router\Group; 15 | use Codeburner\Router\Parser; 16 | use ReflectionClass; 17 | use ReflectionMethod; 18 | use ReflectionParameter; 19 | 20 | /** 21 | * Methods for enable the collector to make routes from a controller. 22 | * 23 | * @author Alex Rohleder 24 | */ 25 | 26 | trait ControllerCollectorTrait 27 | { 28 | 29 | /** 30 | * @return Parser 31 | */ 32 | 33 | abstract public function getParser(); 34 | 35 | /** 36 | * @param string $method 37 | * @param string $pattern 38 | * @param callable $action 39 | * 40 | * @return Group 41 | */ 42 | 43 | abstract public function set($method, $pattern, $action); 44 | 45 | /** 46 | * Define how controller actions names will be joined to form the route pattern. 47 | * 48 | * @var string 49 | */ 50 | 51 | protected $controllerActionJoin = "/"; 52 | 53 | /** 54 | * Maps all the controller methods that begins with a HTTP method, and maps the rest of 55 | * name as a path. The path will be the method name with slashes before every camelcased 56 | * word and without the HTTP method prefix, and the controller name will be used to prefix 57 | * the route pattern. e.g. ArticlesController::getCreate will generate a route to: GET articles/create 58 | * 59 | * @param string $controller The controller name 60 | * @param string $prefix 61 | * 62 | * @throws \ReflectionException 63 | * @return Group 64 | */ 65 | 66 | public function controller($controller, $prefix = null) 67 | { 68 | $controller = new ReflectionClass($controller); 69 | $prefix = $prefix === null ? $this->getControllerPrefix($controller) : $prefix; 70 | $methods = $controller->getMethods(ReflectionMethod::IS_PUBLIC); 71 | return $this->collectControllerRoutes($controller, $methods, "/$prefix/"); 72 | } 73 | 74 | /** 75 | * Maps several controllers at same time. 76 | * 77 | * @param string[] $controllers Controllers name. 78 | * @throws \ReflectionException 79 | * @return Group 80 | */ 81 | 82 | public function controllers(array $controllers) 83 | { 84 | $group = new Group; 85 | foreach ($controllers as $controller) 86 | $group->set($this->controller($controller)); 87 | return $group; 88 | } 89 | 90 | /** 91 | * Alias for Collector::controller but maps a controller without using the controller name as prefix. 92 | * 93 | * @param string $controller The controller name 94 | * @throws \ReflectionException 95 | * @return Group 96 | */ 97 | 98 | public function controllerWithoutPrefix($controller) 99 | { 100 | $controller = new ReflectionClass($controller); 101 | $methods = $controller->getMethods(ReflectionMethod::IS_PUBLIC); 102 | return $this->collectControllerRoutes($controller, $methods, "/"); 103 | } 104 | 105 | /** 106 | * Alias for Collector::controllers but maps a controller without using the controller name as prefix. 107 | * 108 | * @param string[] $controllers 109 | * @throws \ReflectionException 110 | * @return Group 111 | */ 112 | 113 | public function controllersWithoutPrefix(array $controllers) 114 | { 115 | $group = new Group; 116 | foreach ($controllers as $controller) 117 | $group->set($this->controllerWithoutPrefix($controller)); 118 | return $group; 119 | } 120 | 121 | /** 122 | * @param ReflectionClass $controller 123 | * @param ReflectionMethod[] $methods 124 | * @param string $prefix 125 | * 126 | * @return Group 127 | */ 128 | 129 | protected function collectControllerRoutes(ReflectionClass $controller, array $methods, $prefix) 130 | { 131 | $group = new Group; 132 | $controllerDefaultStrategy = $this->getAnnotatedStrategy($controller); 133 | 134 | foreach ($methods as $method) { 135 | $name = preg_split("~(?=[A-Z])~", $method->name); 136 | $http = $name[0]; 137 | unset($name[0]); 138 | 139 | if (strpos(Collector::HTTP_METHODS, $http) !== false) { 140 | $action = $prefix . strtolower(implode($this->controllerActionJoin, $name)); 141 | $dynamic = $this->getMethodConstraints($method); 142 | $strategy = $this->getAnnotatedStrategy($method); 143 | 144 | $route = $this->set($http, "$action$dynamic", [$controller->name, $method->name]); 145 | 146 | if ($strategy !== null) { 147 | $route->setStrategy($strategy); 148 | } else $route->setStrategy($controllerDefaultStrategy); 149 | 150 | $group->set($route); 151 | } 152 | } 153 | 154 | return $group; 155 | } 156 | 157 | /** 158 | * @param ReflectionClass $controller 159 | * 160 | * @return string 161 | */ 162 | 163 | protected function getControllerPrefix(ReflectionClass $controller) 164 | { 165 | preg_match("~\@prefix\s([a-zA-Z\\\_]+)~i", (string) $controller->getDocComment(), $prefix); 166 | return isset($prefix[1]) ? $prefix[1] : str_replace("controller", "", strtolower($controller->getShortName())); 167 | } 168 | 169 | /** 170 | * @param \ReflectionMethod 171 | * @return string 172 | */ 173 | 174 | protected function getMethodConstraints(ReflectionMethod $method) 175 | { 176 | $beginPath = ""; 177 | $endPath = ""; 178 | 179 | if ($parameters = $method->getParameters()) { 180 | $types = $this->getParamsConstraint($method); 181 | 182 | foreach ($parameters as $parameter) { 183 | if ($parameter->isOptional()) { 184 | $beginPath .= "["; 185 | $endPath .= "]"; 186 | } 187 | 188 | $beginPath .= $this->getPathConstraint($parameter, $types); 189 | } 190 | } 191 | 192 | return $beginPath . $endPath; 193 | } 194 | 195 | /** 196 | * @param ReflectionParameter $parameter 197 | * @param string[] $types 198 | * @return string 199 | */ 200 | 201 | protected function getPathConstraint(ReflectionParameter $parameter, $types) 202 | { 203 | $name = $parameter->name; 204 | $path = "/{" . $name; 205 | return isset($types[$name]) ? "$path:{$types[$name]}}" : "$path}"; 206 | } 207 | 208 | /** 209 | * @param ReflectionMethod $method 210 | * @return string[] 211 | */ 212 | 213 | protected function getParamsConstraint(ReflectionMethod $method) 214 | { 215 | $params = []; 216 | preg_match_all("~\@param\s(" . implode("|", array_keys($this->getParser()->getWildcards())) . "|\(.+\))\s\\$([a-zA-Z0-1_]+)~i", 217 | $method->getDocComment(), $types, PREG_SET_ORDER); 218 | 219 | foreach ((array) $types as $type) { 220 | // if a pattern is defined on Match take it otherwise take the param type by PHPDoc. 221 | $params[$type[2]] = isset($type[4]) ? $type[4] : $type[1]; 222 | } 223 | 224 | return $params; 225 | } 226 | 227 | /** 228 | * @param ReflectionClass|ReflectionMethod $reflector 229 | * @return string|null 230 | */ 231 | 232 | protected function getAnnotatedStrategy($reflector) 233 | { 234 | preg_match("~\@strategy\s([a-zA-Z\\\_]+)~i", (string) $reflector->getDocComment(), $strategy); 235 | return isset($strategy[1]) ? $strategy[1] : null; 236 | } 237 | 238 | /** 239 | * Define how controller actions names will be joined to form the route pattern. 240 | * Defaults to "/" so actions like "getMyAction" will be "/my/action". If changed to 241 | * "-" the new pattern will be "/my-action". 242 | * 243 | * @param string $join 244 | */ 245 | 246 | public function setControllerActionJoin($join) 247 | { 248 | $this->controllerActionJoin = $join; 249 | } 250 | 251 | /** 252 | * @return string 253 | */ 254 | 255 | public function getControllerActionJoin() 256 | { 257 | return $this->controllerActionJoin; 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/Collectors/ResourceCollectorTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Collectors; 12 | 13 | /** 14 | * Just a fix to phpstorm parser, as it intends that Resource is 15 | * a datatype of php7 and not a class in router package. 16 | */ 17 | 18 | use Codeburner\Router\Resource as RouteResource; 19 | 20 | /** 21 | * Methods for enable the collector to be resourceful and make 22 | * easily to build apis routes. 23 | * 24 | * @author Alex Rohleder 25 | */ 26 | 27 | trait ResourceCollectorTrait 28 | { 29 | 30 | /** 31 | * @param string $method 32 | * @param string $pattern 33 | * @param string $action 34 | * 35 | * @return \Codeburner\Router\Group 36 | */ 37 | 38 | abstract public function set($method, $pattern, $action); 39 | 40 | /** 41 | * A map of all routes of resources. 42 | * 43 | * @var array 44 | */ 45 | 46 | protected $map = [ 47 | "index" => ["get", "/{name}"], 48 | "make" => ["get", "/{name}/make"], 49 | "create" => ["post", "/{name}"], 50 | "show" => ["get", "/{name}/{id:int+}"], 51 | "edit" => ["get", "/{name}/{id:int+}/edit"], 52 | "update" => ["put", "/{name}/{id:int+}"], 53 | "delete" => ["delete", "/{name}/{id:int+}"], 54 | ]; 55 | 56 | /** 57 | * Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. 58 | * Instead of declaring separate routes for your index, show, new, edit, create, update and destroy actions, 59 | * a resourceful route declares them in a single line of code. 60 | * 61 | * @param string $controller The controller name. 62 | * @param array $options Some options like, "as" to name the route pattern, "only" to 63 | * explicit say that only this routes will be registered, and 64 | * "except" that register all the routes except the indicates. 65 | * @return RouteResource 66 | */ 67 | 68 | public function resource($controller, array $options = array()) 69 | { 70 | $name = isset($options["prefix"]) ? $options["prefix"] : ""; 71 | $name .= $this->getResourceName($controller, $options); 72 | $actions = $this->getResourceActions($options); 73 | $resource = new RouteResource; 74 | 75 | foreach ($actions as $action => $map) { 76 | $resource->set( 77 | $this->set( 78 | $map[0], 79 | $this->getResourcePath($action, $map[1], $name, $options), 80 | [$controller, $action] 81 | ) 82 | ->setName("$name.$action") 83 | ); 84 | } 85 | 86 | return $resource; 87 | } 88 | 89 | /** 90 | * Collect several resources at same time. 91 | * 92 | * @param array $controllers Several controller names as parameters or an array with all controller names. 93 | * @return RouteResource 94 | */ 95 | 96 | public function resources(array $controllers) 97 | { 98 | $resource = new RouteResource; 99 | foreach ($controllers as $controller) 100 | $resource->set($this->resource($controller)); 101 | return $resource; 102 | } 103 | 104 | /** 105 | * @param string $controller 106 | * @param array $options 107 | * 108 | * @return mixed 109 | */ 110 | 111 | protected function getResourceName($controller, array $options) 112 | { 113 | return isset($options["as"]) ? $options["as"] : str_replace("controller", "", strtolower($controller)); 114 | } 115 | 116 | /** 117 | * @param array $options 118 | * @return array 119 | */ 120 | 121 | protected function getResourceActions(array $options) 122 | { 123 | return isset($options["only"]) ? array_intersect_key($this->map, array_flip((array) $options["only"])) : 124 | (isset($options["except"]) ? array_diff_key($this->map, array_flip((array) $options["except"])) : $this->map); 125 | } 126 | 127 | /** 128 | * @param string $action 129 | * @param string $path 130 | * @param string $name 131 | * @param string[] $options 132 | * 133 | * @return string 134 | */ 135 | 136 | protected function getResourcePath($action, $path, $name, array $options) 137 | { 138 | return str_replace("{name}", $name, 139 | $action === "make" && isset($options["translate"]["make"]) ? str_replace("make", $options["translate"]["make"], $path) : 140 | ($action === "edit" && isset($options["translate"]["edit"]) ? str_replace("edit", $options["translate"]["edit"], $path) : $path)); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/Exceptions/BadRouteException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions; 12 | 13 | /** 14 | * Exception base, thrown when a route pattern cannot be parsed, or is wrong formatted. 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class BadRouteException extends \Exception 20 | { 21 | 22 | const OPTIONAL_SEGMENTS_ON_MIDDLE = "Optional segments can only occur at the end of a route."; 23 | const UNCLOSED_OPTIONAL_SEGMENTS = "Number of opening [ and closing ] does not match."; 24 | const EMPTY_OPTIONAL_PARTS = "Empty optional part."; 25 | const WRONG_CONTAINER_WRAPPER_FUNC = "The container wrapper function passed to `call` method in `Codeburner\\Router\\Route` is not callable."; 26 | const BAD_STRATEGY = "`%s` is not a valid route dispatch strategy, it must implement the `Codeburner\\Router\\Strategies\\StrategyInterface` interface."; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Exceptions/Http/BadRequestException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * BadRequestException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class BadRequestException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 400; 23 | protected $message = "Bad Request"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/ConflictException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * ConflictException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class ConflictException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 409; 23 | protected $message = "Conflict"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/ForbiddenException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * ForbiddenException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class ForbiddenException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 403; 23 | protected $message = "Forbidden"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/GoneException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * GoneException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class GoneException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 410; 23 | protected $message = "Gone"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/HttpExceptionAbstract.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | use Exception; 14 | use Psr\Http\Message\ResponseInterface; 15 | 16 | /** 17 | * Class HttpExceptionAbstract 18 | * 19 | * @author Alex Rohleder 20 | */ 21 | 22 | abstract class HttpExceptionAbstract extends Exception 23 | { 24 | 25 | public function getResponse(ResponseInterface $response) 26 | { 27 | return $response->withStatus($this->code, $this->message); 28 | } 29 | 30 | public function getJsonResponse(ResponseInterface $response) 31 | { 32 | $response->withAddedHeader("content-type", "application/json"); 33 | 34 | if ($response->getBody()->isWritable()) { 35 | $response->getBody()->write(json_encode(["status-code" => $this->code, "reason-phrase" => $this->message])); 36 | } 37 | 38 | return $this->getResponse($response); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/Exceptions/Http/LengthRequiredException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * LengthRequiredException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class LengthRequiredException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 411; 23 | protected $message = "Length Required"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/MethodNotAllowedException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * MethodNotAllowed 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class MethodNotAllowedException extends HttpExceptionAbstract 20 | { 21 | 22 | /** 23 | * @var string 24 | */ 25 | 26 | protected $requestedMethod; 27 | 28 | /** 29 | * @var string 30 | */ 31 | 32 | protected $requestedUri; 33 | 34 | /** 35 | * @var string[] 36 | */ 37 | 38 | protected $allowedMethods; 39 | 40 | /** 41 | * MethodNotAllowedException constructor. 42 | * 43 | * @param string $requestedMethod 44 | * @param string $requestedUri 45 | * @param string[] $allowedMethods 46 | */ 47 | 48 | public function __construct($requestedMethod, $requestedUri, array $allowedMethods) 49 | { 50 | $this->requestedMethod = $requestedMethod; 51 | $this->requestedUri = $requestedUri; 52 | $this->allowedMethods = $allowedMethods; 53 | $this->code = 405; 54 | $this->message = "Method Not Allowed"; 55 | } 56 | 57 | /** 58 | * Verify if the given HTTP method is allowed for the request. 59 | * 60 | * @param string $method An HTTP method 61 | * @return bool 62 | */ 63 | 64 | public function can($method) 65 | { 66 | return in_array(strtolower($method), $this->allowedMethods); 67 | } 68 | 69 | /** 70 | * The HTTP specification requires that a 405 Method Not Allowed response include the 71 | * Allow: header to detail available methods for the requested resource. 72 | * 73 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html section 14.7 74 | * @return string 75 | */ 76 | 77 | public function allowed() 78 | { 79 | return implode(', ', $this->allowedMethods); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Exceptions/Http/NotAcceptableException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * NotAcceptableException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class NotAcceptableException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 406; 23 | protected $message = "Not Acceptable"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * NotFoundException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class NotFoundException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 404; 23 | protected $message = "Not Found"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/PaymentRequiredException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * PaymentRequiredException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class PaymentRequiredException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 402; 23 | protected $message = "Payment Required"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/PreconditionFailedException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * PreconditionFailedException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class PreconditionFailedException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 412; 23 | protected $message = "Precondition Failed"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/RequestTimeOutException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * RequestTimeOutException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class RequestTimeOutException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 408; 23 | protected $message = "Request Time Out"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/ServiceUnavailableException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * ServiceUnavailableException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class ServiceUnavailableException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 503; 23 | protected $message = "Service Unavailable"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/UnauthorizedException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * UnauthorizedException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class UnauthorizedException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 401; 23 | protected $message = "Unauthorized Exception"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/Http/UnsupportedMediaTypeException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions\Http; 12 | 13 | /** 14 | * UnsupportedMediaTypeException 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class UnsupportedMediaTypeException extends HttpExceptionAbstract 20 | { 21 | 22 | protected $code = 415; 23 | protected $message = "Unsupported Media Type"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/MethodNotSupportedException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Exceptions; 12 | 13 | /** 14 | * Exception thrown when none route is matched, but a similar route is found in another HTTP method. 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class MethodNotSupportedException extends \Exception 20 | { 21 | 22 | public function __construct($method) { 23 | $this->message = "The HTTP method '$method' is not supported by the route collector."; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Group.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router; 12 | 13 | /** 14 | * Group several routes and abstract operations applied to all. 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class Group 20 | { 21 | 22 | /** 23 | * All grouped route objects. 24 | * 25 | * @var Route[] 26 | */ 27 | 28 | protected $routes; 29 | 30 | /** 31 | * Set a new Route or merge an existing group of routes. 32 | * 33 | * @param Group|Route $route 34 | * @return self 35 | */ 36 | 37 | public function set($route) 38 | { 39 | if ($route instanceof Group) { 40 | foreach ($route->all() as $r) 41 | $this->routes[] = $r; 42 | } else $this->routes[] = $route; 43 | return $this; 44 | } 45 | 46 | /** 47 | * A fast way to register a route into the group 48 | * 49 | * @param Route $route 50 | * @return self 51 | */ 52 | 53 | public function setRoute(Route $route) 54 | { 55 | $this->routes[] = $route; 56 | return $this; 57 | } 58 | 59 | /** 60 | * Return all grouped routes objects. 61 | * 62 | * @return Route[] 63 | */ 64 | 65 | public function all() 66 | { 67 | return $this->routes; 68 | } 69 | 70 | /** 71 | * Get a specific route of the group, routes receive a key based on 72 | * the order they are added to the group. 73 | * 74 | * @param int $number 75 | * @return Route 76 | */ 77 | 78 | public function nth($number) 79 | { 80 | return $this->routes[$number]; 81 | } 82 | 83 | /** 84 | * Forget the registration of all grouped routes on to collector. 85 | * After the forget the route object will still exist but will not 86 | * count for the matcher. 87 | * 88 | * @return self 89 | */ 90 | 91 | public function forget() 92 | { 93 | foreach ($this->routes as $route) 94 | $route->forget(); 95 | return $this; 96 | } 97 | 98 | /** 99 | * Set one HTTP method to all grouped routes. 100 | * 101 | * @param string $method The HTTP Method 102 | * @return self 103 | */ 104 | 105 | public function setMethod($method) 106 | { 107 | foreach ($this->routes as $route) 108 | $route->setMethod($method); 109 | return $this; 110 | } 111 | 112 | /** 113 | * Set one action to all grouped routes. 114 | * 115 | * @param string $action 116 | * @return self 117 | */ 118 | 119 | public function setAction($action) 120 | { 121 | foreach ($this->routes as $route) 122 | $route->setAction($action); 123 | return $this; 124 | } 125 | 126 | /** 127 | * Set one namespace to all grouped routes. 128 | * 129 | * @param string $namespace 130 | * @return self 131 | */ 132 | 133 | public function setNamespace($namespace) 134 | { 135 | foreach ($this->routes as $route) 136 | $route->setNamespace($namespace); 137 | return $this; 138 | } 139 | 140 | /** 141 | * Add a prefix to all grouped routes pattern. 142 | * 143 | * @param string $prefix 144 | * @return self 145 | */ 146 | 147 | public function setPrefix($prefix) 148 | { 149 | $prefix = "/" . ltrim($prefix, "/"); 150 | $routes = []; 151 | foreach ($this->routes as $route) 152 | $routes[] = $route->setPattern(rtrim($prefix . $route->getPattern(), "/")); 153 | $this->routes = $routes; 154 | return $this; 155 | } 156 | 157 | /** 158 | * Set metadata to all grouped routes. 159 | * 160 | * @param string $key 161 | * @param string $value 162 | * 163 | * @return $this 164 | */ 165 | 166 | public function setMetadata($key, $value) 167 | { 168 | foreach ($this->routes as $route) 169 | $route->setMetadata($key, $value); 170 | return $this; 171 | } 172 | 173 | /** 174 | * Set a bunch of metadata to all grouped routes. 175 | * 176 | * @param mixed[] $metadata 177 | * @return $this 178 | */ 179 | 180 | public function setMetadataArray(array $metadata) 181 | { 182 | foreach ($this->routes as $route) 183 | $route->setMetadataArray($metadata); 184 | return $this; 185 | } 186 | 187 | /** 188 | * Set default parameters to all grouped routes. 189 | * 190 | * @param mixed[] $defaults 191 | * @return $this 192 | */ 193 | 194 | public function setDefaults(array $defaults) 195 | { 196 | foreach ($this->routes as $route) 197 | $route->setDefaults($defaults); 198 | return $this; 199 | } 200 | 201 | /** 202 | * Set a default parameter to all grouped routes. 203 | * 204 | * @param string $key 205 | * @param mixed $value 206 | * 207 | * @return $this 208 | */ 209 | 210 | public function setDefault($key, $value) 211 | { 212 | foreach ($this->routes as $route) 213 | $route->setDefault($key, $value); 214 | return $this; 215 | } 216 | 217 | /** 218 | * Set one dispatch strategy to all grouped routes. 219 | * 220 | * @param string|Strategies\StrategyInterface $strategy 221 | * @return self 222 | */ 223 | 224 | public function setStrategy($strategy) 225 | { 226 | foreach ($this->routes as $route) 227 | $route->setStrategy($strategy); 228 | return $this; 229 | } 230 | 231 | /** 232 | * Replace or define a constraint for all dynamic segments named by $name. 233 | * 234 | * @param string $name 235 | * @param string $regex 236 | * 237 | * @return self 238 | */ 239 | 240 | public function setConstraint($name, $regex) 241 | { 242 | foreach ($this->routes as $route) 243 | $route->setConstraint($name, $regex); 244 | return $this; 245 | } 246 | 247 | /** 248 | * Set a name to a Route. 249 | * 250 | * @param string $name 251 | * @return self 252 | */ 253 | 254 | public function setName($name) 255 | { 256 | if (count($this->routes) > 1) { 257 | throw new \LogicException("You cannot set the same name to several routes."); 258 | } 259 | 260 | $this->routes[0]->setName($name); 261 | return $this; 262 | } 263 | 264 | } 265 | -------------------------------------------------------------------------------- /src/Matcher.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router; 12 | 13 | use Codeburner\Router\Exceptions\Http\MethodNotAllowedException; 14 | use Codeburner\Router\Exceptions\Http\NotFoundException; 15 | use Exception; 16 | 17 | /** 18 | * The matcher class find the route for a given http method and path. 19 | * 20 | * @author Alex Rohleder 21 | */ 22 | 23 | class Matcher 24 | { 25 | 26 | /** 27 | * @var Collector 28 | */ 29 | 30 | protected $collector; 31 | 32 | /** 33 | * @var Parser $parser 34 | */ 35 | 36 | protected $parser; 37 | 38 | /** 39 | * Define a basepath to all routes. 40 | * 41 | * @var string 42 | */ 43 | 44 | protected $basepath = ""; 45 | 46 | /** 47 | * Construct the route dispatcher. 48 | * 49 | * @param Collector $collector 50 | * @param string $basepath Define a Path prefix that must be excluded on matches. 51 | */ 52 | 53 | public function __construct(Collector $collector, $basepath = "") 54 | { 55 | $this->collector = $collector; 56 | $this->basepath = $basepath; 57 | } 58 | 59 | /** 60 | * Find a route that matches the given arguments. 61 | * 62 | * @param string $httpMethod 63 | * @param string $path 64 | * 65 | * @throws NotFoundException 66 | * @throws MethodNotAllowedException 67 | * 68 | * @return Route 69 | */ 70 | 71 | public function match($httpMethod, $path) 72 | { 73 | $path = $this->parsePath($path); 74 | 75 | if (($route = $this->collector->findStaticRoute($httpMethod, $path)) || 76 | ($route = $this->matchDynamicRoute($httpMethod, $path))) { 77 | $route->setMatcher($this); 78 | 79 | return $route; 80 | } 81 | 82 | $this->matchSimilarRoute($httpMethod, $path); 83 | } 84 | 85 | /** 86 | * Find and return the request dynamic route based on the compiled data and Path. 87 | * 88 | * @param string $httpMethod 89 | * @param string $path 90 | * 91 | * @return Route|false If the request match an array with the action and parameters will 92 | * be returned otherwise a false will. 93 | */ 94 | 95 | protected function matchDynamicRoute($httpMethod, $path) 96 | { 97 | if ($routes = $this->collector->findDynamicRoutes($httpMethod, $path)) { 98 | // cache the parser reference 99 | $this->parser = $this->collector->getParser(); 100 | // chunk routes for smaller regex groups using the Sturges' Formula 101 | foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk) { 102 | array_map([$this, "buildRoute"], $chunk); 103 | list($pattern, $map) = $this->buildGroup($chunk); 104 | 105 | if (!preg_match($pattern, $path, $matches)) { 106 | continue; 107 | } 108 | 109 | /** @var Route $route */ 110 | $route = $map[count($matches)]; 111 | unset($matches[0]); 112 | 113 | $route->setParams(array_combine($route->getParams(), array_filter($matches))); 114 | 115 | return $route; 116 | } 117 | } 118 | 119 | return false; 120 | } 121 | 122 | /** 123 | * Parse the dynamic segments of the pattern and replace then for 124 | * corresponding regex. 125 | * 126 | * @param Route $route 127 | * @return Route 128 | */ 129 | 130 | protected function buildRoute(Route $route) 131 | { 132 | if ($route->getBlock()) { 133 | return $route; 134 | } 135 | 136 | list($pattern, $params) = $this->parsePlaceholders($route->getPattern()); 137 | return $route->setPatternWithoutReset($pattern)->setParams($params)->setBlock(true); 138 | } 139 | 140 | /** 141 | * Group several dynamic routes patterns into one big regex and maps 142 | * the routes to the pattern positions in the big regex. 143 | * 144 | * @param Route[] $routes 145 | * @return array 146 | */ 147 | 148 | protected function buildGroup(array $routes) 149 | { 150 | $groupCount = (int) $map = $regex = []; 151 | 152 | foreach ($routes as $route) { 153 | $params = $route->getParams(); 154 | $paramsCount = count($params); 155 | $groupCount = max($groupCount, $paramsCount) + 1; 156 | $regex[] = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1); 157 | $map[$groupCount] = $route; 158 | } 159 | 160 | return ["~^(?|" . implode("|", $regex) . ")$~", $map]; 161 | } 162 | 163 | /** 164 | * Parse an route pattern seeking for parameters and build the route regex. 165 | * 166 | * @param string $pattern 167 | * @return array 0 => new route regex, 1 => map of parameter names 168 | */ 169 | 170 | protected function parsePlaceholders($pattern) 171 | { 172 | $params = []; 173 | $parser = $this->parser; 174 | 175 | preg_match_all("~" . $parser::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER); 176 | 177 | foreach ((array) $matches as $key => $match) { 178 | $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern); 179 | $params[$key] = $match[1]; 180 | } 181 | 182 | return [$pattern, $params]; 183 | } 184 | 185 | /** 186 | * Get only the path of a given url. 187 | * 188 | * @param string $path The given URL 189 | * 190 | * @throws Exception 191 | * @return string 192 | */ 193 | 194 | protected function parsePath($path) 195 | { 196 | $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH); 197 | 198 | if ($path === false) { 199 | throw new Exception("Seriously malformed URL passed to route matcher."); 200 | } 201 | 202 | return $path; 203 | } 204 | 205 | /** 206 | * Generate an HTTP error request with method not allowed or not found. 207 | * 208 | * @param string $httpMethod 209 | * @param string $path 210 | * 211 | * @throws NotFoundException 212 | * @throws MethodNotAllowedException 213 | */ 214 | 215 | protected function matchSimilarRoute($httpMethod, $path) 216 | { 217 | $dm = []; 218 | 219 | if (($sm = $this->checkStaticRouteInOtherMethods($httpMethod, $path)) 220 | || ($dm = $this->checkDynamicRouteInOtherMethods($httpMethod, $path))) { 221 | throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm)); 222 | } 223 | 224 | throw new NotFoundException; 225 | } 226 | 227 | /** 228 | * Verify if a static route match in another method than the requested. 229 | * 230 | * @param string $targetHttpMethod The HTTP method that must not be checked 231 | * @param string $path The Path that must be matched. 232 | * 233 | * @return array 234 | */ 235 | 236 | protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path) 237 | { 238 | return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { 239 | return (bool) $this->collector->findStaticRoute($httpMethod, $path); 240 | }); 241 | } 242 | 243 | /** 244 | * Verify if a dynamic route match in another method than the requested. 245 | * 246 | * @param string $targetHttpMethod The HTTP method that must not be checked 247 | * @param string $path The Path that must be matched. 248 | * 249 | * @return array 250 | */ 251 | 252 | protected function checkDynamicRouteInOtherMethods($targetHttpMethod, $path) 253 | { 254 | return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { 255 | return (bool) $this->matchDynamicRoute($httpMethod, $path); 256 | }); 257 | } 258 | 259 | /** 260 | * Strip the given http methods and return all the others. 261 | * 262 | * @param string|string[] 263 | * @return array 264 | */ 265 | 266 | protected function getHttpMethodsBut($targetHttpMethod) 267 | { 268 | return array_diff(explode(" ", Collector::HTTP_METHODS), (array) $targetHttpMethod); 269 | } 270 | 271 | /** 272 | * @return Collector 273 | */ 274 | 275 | public function getCollector() 276 | { 277 | return $this->collector; 278 | } 279 | 280 | /** 281 | * @return string 282 | */ 283 | 284 | public function getBasePath() 285 | { 286 | return $this->basepath; 287 | } 288 | 289 | /** 290 | * Set a new basepath, this will be a prefix that must be excluded in 291 | * every requested Path. 292 | * 293 | * @param string $basepath The new basepath 294 | */ 295 | 296 | public function setBasePath($basepath) 297 | { 298 | $this->basepath = $basepath; 299 | } 300 | 301 | } 302 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router; 12 | 13 | use Codeburner\Router\Exceptions\BadRouteException; 14 | 15 | /** 16 | * All the parsing route paths logic are maintained by this class. 17 | * 18 | * @author Alex Rohleder 19 | */ 20 | 21 | class Parser 22 | { 23 | 24 | /** 25 | * These regex define the structure of a dynamic segment in a pattern. 26 | * 27 | * @var string 28 | */ 29 | 30 | const DYNAMIC_REGEX = "{\s*(\w*)\s*(?::\s*([^{}]*(?:{(?-1)}*)*))?\s*}"; 31 | 32 | /** 33 | * Some regex wildcards for easily definition of dynamic routes. ps. all keys and values must start with : 34 | * 35 | * @var array 36 | */ 37 | 38 | protected $wildcards = [ 39 | ":uid" => ":uid-[a-zA-Z0-9]", 40 | ":slug" => ":[a-z0-9-]", 41 | ":string" => ":\w", 42 | ":int" => ":\d", 43 | ":integer" => ":\d", 44 | ":float" => ":[-+]?\d*?[.]?\d", 45 | ":double" => ":[-+]?\d*?[.]?\d", 46 | ":hex" => ":0[xX][0-9a-fA-F]", 47 | ":octal" => ":0[1-7][0-7]", 48 | ":bool" => ":1|0|true|false|yes|no", 49 | ":boolean" => ":1|0|true|false|yes|no", 50 | ]; 51 | 52 | /** 53 | * Separate routes pattern with optional parts into n new patterns. 54 | * 55 | * @param string $pattern 56 | * 57 | * @throws BadRouteException 58 | * @return array 59 | */ 60 | 61 | public function parsePattern($pattern) 62 | { 63 | $withoutClosing = rtrim($pattern, "]"); 64 | $closingNumber = strlen($pattern) - strlen($withoutClosing); 65 | 66 | $segments = preg_split("~" . self::DYNAMIC_REGEX . "(*SKIP)(*F)|\[~x", $withoutClosing); 67 | $this->parseSegments($segments, $closingNumber, $withoutClosing); 68 | 69 | return $this->buildSegments($segments); 70 | } 71 | 72 | /** 73 | * Parse all the possible patterns seeking for an incorrect or incompatible pattern. 74 | * 75 | * @param string[] $segments Segments are all the possible patterns made on top of a pattern with optional segments. 76 | * @param int $closingNumber The count of optional segments. 77 | * @param string $withoutClosing The pattern without the closing token of an optional segment. aka: ] 78 | * 79 | * @throws BadRouteException 80 | */ 81 | 82 | protected function parseSegments(array $segments, $closingNumber, $withoutClosing) 83 | { 84 | if ($closingNumber !== count($segments) - 1) { 85 | if (preg_match("~" . self::DYNAMIC_REGEX . "(*SKIP)(*F)|\]~x", $withoutClosing)) { 86 | throw new BadRouteException(BadRouteException::OPTIONAL_SEGMENTS_ON_MIDDLE); 87 | } else throw new BadRouteException(BadRouteException::UNCLOSED_OPTIONAL_SEGMENTS); 88 | } 89 | } 90 | 91 | /** 92 | * @param string[] $segments 93 | * 94 | * @throws BadRouteException 95 | * @return array 96 | */ 97 | 98 | protected function buildSegments(array $segments) 99 | { 100 | $pattern = ""; 101 | $patterns = []; 102 | $wildcardTokens = array_keys($this->wildcards); 103 | $wildcardRegex = $this->wildcards; 104 | 105 | foreach ($segments as $n => $segment) { 106 | if ($segment === "" && $n !== 0) { 107 | throw new BadRouteException(BadRouteException::EMPTY_OPTIONAL_PARTS); 108 | } 109 | 110 | $patterns[] = $pattern .= str_replace($wildcardTokens, $wildcardRegex, $segment); 111 | } 112 | 113 | return $patterns; 114 | } 115 | 116 | /** 117 | * @return string[] 118 | */ 119 | 120 | public function getWildcards() 121 | { 122 | $wildcards = []; 123 | foreach ($this->wildcards as $token => $regex) 124 | $wildcards[substr($token, 1)] = substr($regex, 1); 125 | return $wildcards; 126 | } 127 | 128 | /** 129 | * @return string[] 130 | */ 131 | 132 | public function getWildcardTokens() 133 | { 134 | return $this->wildcards; 135 | } 136 | 137 | /** 138 | * @param string $wildcard 139 | * @return string|null 140 | */ 141 | 142 | public function getWildcard($wildcard) 143 | { 144 | return isset($this->wildcards[":$wildcard"]) ? substr($this->wildcards[":$wildcard"], 1) : null; 145 | } 146 | 147 | /** 148 | * @param string $wildcard 149 | * @param string $pattern 150 | * 151 | * @return self 152 | */ 153 | 154 | public function setWildcard($wildcard, $pattern) 155 | { 156 | $this->wildcards[":$wildcard"] = ":$pattern"; 157 | return $this; 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/Path.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router; 12 | 13 | /** 14 | * Create paths based on registered routes. 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | class Path 20 | { 21 | 22 | /** 23 | * @var Collector 24 | */ 25 | 26 | protected $collector; 27 | 28 | /** 29 | * Link constructor. 30 | * 31 | * @param Collector $collector 32 | */ 33 | 34 | public function __construct(Collector $collector) 35 | { 36 | $this->collector = $collector; 37 | } 38 | 39 | /** 40 | * Generate a path to a route named by $name. 41 | * 42 | * @param string $name 43 | * @param array $args 44 | * 45 | * @throws \BadMethodCallException 46 | * @return string 47 | */ 48 | 49 | public function to($name, array $args = []) 50 | { 51 | $route = $this->collector->findNamedRoute($name); 52 | $parser = $this->collector->getParser(); 53 | $pattern = $route->getPattern(); 54 | 55 | preg_match_all("~" . $parser::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER); 56 | 57 | foreach ((array) $matches as $key => $match) { 58 | if (!isset($args[$match[1]])) { 59 | throw new \BadMethodCallException("Missing argument '{$match[1]}' on creation of link for '{$name}' route."); 60 | } 61 | 62 | $pattern = str_replace($match[0], $args[$match[1]], $pattern); 63 | } 64 | 65 | return $pattern; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Resource.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router; 12 | 13 | /** 14 | * Representation of a group of several routes with same 15 | * controller and respecting the resourceful actions. 16 | * 17 | * @author Alex Rohleder 18 | */ 19 | 20 | class Resource extends Group 21 | { 22 | 23 | /** 24 | * @inheritdoc 25 | * @throws \BadMethodCallException 26 | */ 27 | 28 | public function setMethod($method) 29 | { 30 | throw new \BadMethodCallException("Resources can't chance they http method."); 31 | } 32 | 33 | /** 34 | * Remove the routes without the passed methods. 35 | * 36 | * @param string|string[] $methods 37 | * @return self 38 | */ 39 | 40 | public function only($methods) 41 | { 42 | $this->filterByMethod((array) $methods, false); 43 | return $this; 44 | } 45 | 46 | /** 47 | * Remove the routes with the passed methods. 48 | * 49 | * @param string|string[] $methods 50 | * @return self 51 | */ 52 | 53 | public function except($methods) 54 | { 55 | $this->filterByMethod((array) $methods, true); 56 | return $this; 57 | } 58 | 59 | /** 60 | * Forget the grouped routes filtering by http methods. 61 | * 62 | * @param string[] $methods 63 | * @param bool $alt Should remove? 64 | */ 65 | 66 | private function filterByMethod(array $methods, $alt) 67 | { 68 | $methods = array_flip(array_map('strtolower', $methods)); 69 | 70 | foreach ($this->routes as $route) { 71 | if (isset($methods[$route->getAction()[1]]) === $alt) { 72 | $route->forget(); 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Translate the "make" or "edit" from resources path. 79 | * 80 | * @param string[] $translations 81 | * @return self 82 | */ 83 | 84 | public function translate(array $translations) 85 | { 86 | foreach ($this->routes as $route) { 87 | $action = $route->getAction()[1]; 88 | 89 | if ($action === "make" && isset($translations["make"])) { 90 | $route->setPatternWithoutReset(str_replace("make", $translations["make"], $route->getPattern())); 91 | } elseif ($action === "edit" && isset($translations["edit"])) { 92 | $route->setPatternWithoutReset(str_replace("edit", $translations["edit"], $route->getPattern())); 93 | } 94 | } 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Add a route or a group of routes to the resource, it means that 101 | * every added route will now receive the parameters of the resource, like id. 102 | * 103 | * @param Route|Group $route 104 | * @return self 105 | */ 106 | 107 | public function member($route) 108 | { 109 | $resource = new self; 110 | $resource->set($route); 111 | $this->nest($resource); 112 | } 113 | 114 | /** 115 | * Nested routes capture the relation between a resource and another resource. 116 | * 117 | * @param self $resource 118 | * @return self 119 | */ 120 | 121 | public function nest(self $resource) 122 | { 123 | foreach ($this->routes as $route) { 124 | if ($route->getAction()[1] === "show") { 125 | $this->set($resource->forget()->setPrefix($this->getNestedPrefix($route->getPattern()))); break; 126 | } 127 | } 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Nest resources but with only build routes with the minimal amount of information 134 | * to uniquely identify the resource. 135 | * 136 | * @param self $resource 137 | * @return self 138 | */ 139 | 140 | public function shallow(self $resource) 141 | { 142 | $newResource = new self; 143 | $resource->forget(); 144 | $routes = $resource->all(); 145 | 146 | foreach ($routes as $route) { 147 | if (strpos("index make create", $route->getAction()[1]) !== false) { 148 | $newResource->set($route); 149 | } 150 | } 151 | 152 | return $this->nest($newResource); 153 | } 154 | 155 | /** 156 | * Resolve the nesting pattern, setting the prefixes based on 157 | * parent resources patterns. 158 | * 159 | * @param string $pattern 160 | * @return string 161 | */ 162 | 163 | protected function getNestedPrefix($pattern) 164 | { 165 | $segments = explode("/", $pattern); 166 | $pattern = ""; 167 | 168 | foreach ($segments as $index => $segment) { 169 | if (strpos($segment, "{") === 0) { 170 | $pattern .= "/{" . $segments[$index - 1] . "_" . ltrim($segment, "{"); 171 | } else $pattern .= $segment; 172 | } 173 | 174 | return $pattern; 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/Route.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router; 12 | 13 | use Codeburner\Router\Exceptions\BadRouteException; 14 | use Codeburner\Router\Strategies\MatcherAwareInterface; 15 | use Codeburner\Router\Strategies\StrategyInterface; 16 | 17 | /** 18 | * Route representation, a route must be able to chang and execute itself. 19 | * 20 | * @author Alex Rohleder 21 | */ 22 | 23 | class Route 24 | { 25 | 26 | /** 27 | * @var Collector 28 | */ 29 | 30 | protected $collector; 31 | 32 | /** 33 | * @var string 34 | */ 35 | 36 | protected $method; 37 | 38 | /** 39 | * @var string 40 | */ 41 | 42 | protected $pattern; 43 | 44 | /** 45 | * @var callable 46 | */ 47 | 48 | protected $action; 49 | 50 | /** 51 | * @var string 52 | */ 53 | 54 | protected $namespace = ""; 55 | 56 | /** 57 | * @var string[] 58 | */ 59 | 60 | protected $params = []; 61 | 62 | /** 63 | * Defaults are parameters set by the user, and don't 64 | * appear on the pattern. 65 | * 66 | * @var array 67 | */ 68 | 69 | protected $defaults = []; 70 | 71 | /** 72 | * Metadata can be set to be used on filters, dispatch strategies 73 | * or anywhere the route object is used. 74 | * 75 | * @var array 76 | */ 77 | 78 | protected $metadata = []; 79 | 80 | /** 81 | * @var string|StrategyInterface 82 | */ 83 | 84 | protected $strategy; 85 | 86 | /** 87 | * Blocked routes are dynamic routes selected to pass by the matcher. 88 | * 89 | * @var boolean 90 | */ 91 | 92 | protected $blocked = false; 93 | 94 | /** 95 | * The matcher that dispatched this route. 96 | * 97 | * @var Matcher $matcher 98 | */ 99 | 100 | protected $matcher; 101 | 102 | /** 103 | * Route name, or alias. 104 | * 105 | * @var string $name 106 | */ 107 | 108 | protected $name; 109 | 110 | /** 111 | * The function used to create controllers from name. 112 | * 113 | * @var callable 114 | */ 115 | 116 | protected $controllerCreationFunction; 117 | 118 | /** 119 | * @param Collector $collector 120 | * @param string $method 121 | * @param string $pattern 122 | * @param callable $action 123 | */ 124 | 125 | public function __construct(Collector $collector, $method, $pattern, $action) 126 | { 127 | $this->collector = $collector; 128 | $this->method = $method; 129 | $this->pattern = $pattern; 130 | $this->action = $action; 131 | } 132 | 133 | /** 134 | * Clone this route and set it into the collector. 135 | * 136 | * @return Route 137 | */ 138 | 139 | public function reset() 140 | { 141 | return $this->collector->set($this->method, $this->pattern, $this->action)->nth(0) 142 | ->setStrategy($this->strategy)->setParams($this->params) 143 | ->setDefaults($this->defaults)->setMetadataArray($this->metadata); 144 | } 145 | 146 | /** 147 | * Remove this route from the collector. 148 | * 149 | * @return self 150 | */ 151 | 152 | public function forget() 153 | { 154 | $this->collector->forget($this->method, $this->pattern); 155 | return $this; 156 | } 157 | 158 | /** 159 | * Execute the route action, if no strategy was provided the action 160 | * will be executed by the call_user_func PHP function. 161 | * 162 | * @param callable $container 163 | * @throws BadRouteException 164 | * @return mixed 165 | */ 166 | 167 | public function call(callable $container = null) 168 | { 169 | $this->action = $this->buildCallable($this->action, $container); 170 | 171 | if ($this->strategy === null) { 172 | return call_user_func_array($this->action, array_merge($this->defaults, $this->params)); 173 | } 174 | 175 | if (!is_object($this->strategy)) { 176 | if ($container === null) { 177 | $this->strategy = new $this->strategy; 178 | } else $this->strategy = $container($this->strategy); 179 | } 180 | 181 | return $this->callWithStrategy(); 182 | } 183 | 184 | /** 185 | * Seek for dynamic content in one callable. This allow to use parameters defined on pattern on callable 186 | * definition, eg. "get" "/{resource:string+}/{slug:slug+}" "{resource}::find". 187 | * 188 | * This will snakecase the resource parameter and deal with as a controller, then call the find method. 189 | * A request for "/articles/my-first-article" will execute find method of Articles controller with only 190 | * "my-first-article" as parameter. 191 | * 192 | * @param callable $callable 193 | * @param callable $container 194 | * 195 | * @return callable 196 | */ 197 | 198 | private function buildCallable($callable, $container = null) 199 | { 200 | if (is_string($callable) && strpos($callable, "::")) { 201 | $callable = explode("::", $callable); 202 | } 203 | 204 | if (is_array($callable)) { 205 | if (is_string($callable[0])) { 206 | $callable[0] = $this->parseCallableController($callable[0], $container); 207 | } 208 | 209 | $callable[1] = $this->parseCallablePlaceholders($callable[1]); 210 | } 211 | 212 | return $callable; 213 | } 214 | 215 | /** 216 | * Get the controller object. 217 | * 218 | * @param string $controller 219 | * @param callable $container 220 | * 221 | * @return Object 222 | */ 223 | 224 | private function parseCallableController($controller, $container) 225 | { 226 | $controller = rtrim($this->namespace, "\\") . "\\" . $this->parseCallablePlaceholders($controller); 227 | 228 | if ($container === null) { 229 | return new $controller; 230 | } else return $container($controller); 231 | } 232 | 233 | /** 234 | * Parse and replace dynamic content on route action. 235 | * 236 | * @param string $fragment Part of callable 237 | * @return string 238 | */ 239 | 240 | private function parseCallablePlaceholders($fragment) 241 | { 242 | if (strpos($fragment, "{") !== false) { 243 | foreach ($this->params as $placeholder => $value) { 244 | if (strpos($fragment, "{" . $placeholder . "}") !== false) { 245 | $fragment = str_replace("{" . $placeholder . "}", ucwords(str_replace("-", " ", $value)), $fragment); 246 | } 247 | } 248 | } 249 | 250 | return $fragment; 251 | } 252 | 253 | /** 254 | * Execute the route action with the given strategy. 255 | * 256 | * @throws BadRouteException 257 | * @return mixed 258 | */ 259 | 260 | private function callWithStrategy() 261 | { 262 | if ($this->strategy instanceof StrategyInterface) { 263 | if ($this->strategy instanceof MatcherAwareInterface) { 264 | $this->strategy->setMatcher($this->matcher); 265 | } 266 | 267 | return $this->strategy->call($this); 268 | } 269 | 270 | throw new BadRouteException(str_replace("%s", get_class($this->strategy), BadRouteException::BAD_STRATEGY)); 271 | } 272 | 273 | /** 274 | * @return Collector 275 | */ 276 | 277 | public function getCollector() 278 | { 279 | return $this->collector; 280 | } 281 | 282 | /** 283 | * @return string 284 | */ 285 | 286 | public function getMethod() 287 | { 288 | return $this->method; 289 | } 290 | 291 | /** 292 | * @return string 293 | */ 294 | 295 | public function getPattern() 296 | { 297 | return $this->pattern; 298 | } 299 | 300 | /** 301 | * @return string[] 302 | */ 303 | 304 | public function getSegments() 305 | { 306 | return explode("/", $this->pattern); 307 | } 308 | 309 | /** 310 | * @return callable 311 | */ 312 | 313 | public function getAction() 314 | { 315 | return $this->action; 316 | } 317 | 318 | /** 319 | * @return string 320 | */ 321 | 322 | public function getNamespace() 323 | { 324 | return $this->namespace; 325 | } 326 | 327 | /** 328 | * @return string[] 329 | */ 330 | 331 | public function getParams() 332 | { 333 | return $this->params; 334 | } 335 | 336 | /** 337 | * @param string $key 338 | * @return string 339 | */ 340 | 341 | public function getParam($key) 342 | { 343 | return $this->params[$key]; 344 | } 345 | 346 | /** 347 | * Return defaults and params merged in one array. 348 | * 349 | * @return array 350 | */ 351 | 352 | public function getMergedParams() 353 | { 354 | return array_merge($this->defaults, $this->params); 355 | } 356 | 357 | /** 358 | * @return array 359 | */ 360 | 361 | public function getDefaults() 362 | { 363 | return $this->defaults; 364 | } 365 | 366 | /** 367 | * @param string $key 368 | * @return mixed 369 | */ 370 | 371 | public function getDefault($key) 372 | { 373 | return $this->defaults[$key]; 374 | } 375 | 376 | /** 377 | * @return array 378 | */ 379 | 380 | public function getMetadataArray() 381 | { 382 | return $this->metadata; 383 | } 384 | 385 | /** 386 | * @param string $key 387 | * @return mixed 388 | */ 389 | 390 | public function getMetadata($key) 391 | { 392 | return $this->metadata[$key]; 393 | } 394 | 395 | /** 396 | * @return string|null 397 | */ 398 | 399 | public function getStrategy() 400 | { 401 | if ($this->strategy instanceof StrategyInterface) { 402 | return get_class($this->strategy); 403 | } 404 | 405 | return $this->strategy; 406 | } 407 | 408 | /** 409 | * @return StrategyInterface|string 410 | */ 411 | 412 | public function getRawStrategy() 413 | { 414 | return $this->strategy; 415 | } 416 | 417 | /** 418 | * @return Matcher 419 | */ 420 | 421 | public function getMatcher() 422 | { 423 | return $this->matcher; 424 | } 425 | 426 | /** 427 | * @return string 428 | */ 429 | 430 | public function getName() 431 | { 432 | return $this->name; 433 | } 434 | 435 | /** 436 | * Verify if a Route have already been blocked. 437 | * 438 | * @return boolean 439 | */ 440 | 441 | public function getBlock() 442 | { 443 | return $this->blocked; 444 | } 445 | 446 | /** 447 | * Blocking a route indicate that that route have been selected and 448 | * parsed, now it will be given to the matcher. 449 | * 450 | * @param bool $blocked 451 | * @return self 452 | */ 453 | 454 | public function setBlock($blocked) 455 | { 456 | $this->blocked = $blocked; 457 | return $this; 458 | } 459 | 460 | /** 461 | * @param string $method 462 | * @return Route 463 | */ 464 | 465 | public function setMethod($method) 466 | { 467 | $this->forget(); 468 | $this->method = $method; 469 | return $this->reset(); 470 | } 471 | 472 | /** 473 | * @param string $pattern 474 | * @return Route 475 | */ 476 | 477 | public function setPattern($pattern) 478 | { 479 | $this->forget(); 480 | $this->pattern = $pattern; 481 | return $this->reset(); 482 | } 483 | 484 | /** 485 | * @param string $pattern 486 | * @return self 487 | */ 488 | 489 | public function setPatternWithoutReset($pattern) 490 | { 491 | $this->pattern = $pattern; 492 | return $this; 493 | } 494 | 495 | /** 496 | * @param string $action 497 | * @return self 498 | */ 499 | 500 | public function setAction($action) 501 | { 502 | $this->action = $action; 503 | return $this; 504 | } 505 | 506 | /** 507 | * @param string $namespace 508 | * @return self 509 | */ 510 | 511 | public function setNamespace($namespace) 512 | { 513 | $this->namespace = $namespace; 514 | return $this; 515 | } 516 | 517 | /** 518 | * @param string[] $params 519 | * @return self 520 | */ 521 | 522 | public function setParams(array $params) 523 | { 524 | $this->params = $params; 525 | return $this; 526 | } 527 | 528 | /** 529 | * @param string $key 530 | * @param string $value 531 | * 532 | * @return self 533 | */ 534 | 535 | public function setParam($key, $value) 536 | { 537 | $this->params[$key] = $value; 538 | return $this; 539 | } 540 | 541 | /** 542 | * @param mixed[] $defaults 543 | * @return self 544 | */ 545 | 546 | public function setDefaults(array $defaults) 547 | { 548 | $this->defaults = $defaults; 549 | return $this; 550 | } 551 | 552 | /** 553 | * @param string $key 554 | * @param mixed $value 555 | * 556 | * @return self 557 | */ 558 | 559 | public function setDefault($key, $value) 560 | { 561 | $this->defaults[$key] = $value; 562 | return $this; 563 | } 564 | 565 | /** 566 | * @param mixed[] $metadata 567 | * @return self 568 | */ 569 | 570 | public function setMetadataArray(array $metadata) 571 | { 572 | $this->metadata = $metadata; 573 | return $this; 574 | } 575 | 576 | /** 577 | * @param string $key 578 | * @param mixed $value 579 | * 580 | * @return $this 581 | */ 582 | 583 | public function setMetadata($key, $value) 584 | { 585 | $this->metadata[$key] = $value; 586 | return $this; 587 | } 588 | 589 | /** 590 | * @param null|string|StrategyInterface $strategy 591 | * @return self 592 | */ 593 | 594 | public function setStrategy($strategy) 595 | { 596 | $this->strategy = $strategy; 597 | return $this; 598 | } 599 | 600 | /** 601 | * @param Matcher $matcher 602 | * @return self 603 | */ 604 | 605 | public function setMatcher(Matcher $matcher) 606 | { 607 | $this->matcher = $matcher; 608 | return $this; 609 | } 610 | 611 | public function setName($name) 612 | { 613 | $this->name = $name; 614 | $this->collector->setRouteName($name, $this); 615 | return $this; 616 | } 617 | 618 | /** 619 | * Set a constraint to a token in the route pattern. 620 | * 621 | * @param string $token 622 | * @param string $regex 623 | * 624 | * @return self 625 | */ 626 | 627 | public function setConstraint($token, $regex) 628 | { 629 | $initPos = strpos($this->pattern, "{" . $token); 630 | 631 | if ($initPos !== false) { 632 | $endPos = strpos($this->pattern, "}", $initPos); 633 | $newPattern = substr_replace($this->pattern, "{" . "$token:$regex" . "}", $initPos, $endPos - $initPos + 1); 634 | $wildcards = $this->collector->getParser()->getWildcardTokens(); 635 | $newPattern = str_replace(array_keys($wildcards), $wildcards, $newPattern); 636 | $this->setPatternWithoutReset($newPattern); 637 | } 638 | 639 | return $this; 640 | } 641 | 642 | /** 643 | * @param string $key 644 | * @return bool 645 | */ 646 | 647 | public function hasParam($key) 648 | { 649 | return isset($this->params[$key]); 650 | } 651 | 652 | /** 653 | * @param string $key 654 | * @return bool 655 | */ 656 | 657 | public function hasDefault($key) 658 | { 659 | return isset($this->defaults[$key]); 660 | } 661 | 662 | /** 663 | * @param string $key 664 | * @return bool 665 | */ 666 | 667 | public function hasMetadata($key) 668 | { 669 | return isset($this->metadata[$key]); 670 | } 671 | 672 | } 673 | -------------------------------------------------------------------------------- /src/Strategies/EnhancerAbstractStrategy.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Strategies; 12 | 13 | use Codeburner\Router\Route; 14 | use Codeburner\Router\Exceptions\BadRouteException; 15 | 16 | /** 17 | * The route enhancer strategy act like a bridge between 18 | * one route and it dispatch strategy. In this "bridge" operations 19 | * are made manipulating the route object. 20 | * 21 | * @author Alex Rohleder 22 | */ 23 | 24 | abstract class EnhancerAbstractStrategy implements StrategyInterface 25 | { 26 | 27 | /** 28 | * Key used to store the real route strategy on metadata. 29 | * 30 | * @var string 31 | */ 32 | 33 | protected $metadataStrategyKey = "strategy"; 34 | 35 | /** 36 | * @inheritdoc 37 | * @throws BadRouteException 38 | */ 39 | 40 | public function call(Route $route) 41 | { 42 | if ($route->hasMetadata($this->metadataStrategyKey)) { 43 | $route->setStrategy($route->getMetadata($this->metadataStrategyKey)); 44 | } else $route->setStrategy(null); 45 | 46 | $this->enhance($route); 47 | 48 | return $route->call(); 49 | } 50 | 51 | /** 52 | * Manipulate route object before the dispatch. 53 | * 54 | * @param Route $route 55 | * @return mixed 56 | */ 57 | 58 | abstract public function enhance(Route $route); 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Strategies/MatcherAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Strategies; 12 | 13 | /** 14 | * An interface that represent one object that needs to 15 | * know about the Matcher. 16 | * 17 | * @author Alex Rohleder 18 | */ 19 | 20 | interface MatcherAwareInterface 21 | { 22 | 23 | /** 24 | * @param \Codeburner\Router\Matcher $matcher 25 | */ 26 | 27 | public function setMatcher(\Codeburner\Router\Matcher $matcher); 28 | 29 | /** 30 | * @return \Codeburner\Router\Matcher 31 | */ 32 | 33 | public function getMatcher(); 34 | 35 | } -------------------------------------------------------------------------------- /src/Strategies/RequestAwareTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Strategies; 12 | 13 | use Psr\Http\Message\RequestInterface; 14 | use Psr\Http\Message\ResponseInterface; 15 | 16 | /** 17 | * Trait RequestAwareTrait 18 | * 19 | * @author Alex Rohleder 20 | */ 21 | 22 | trait RequestAwareTrait 23 | { 24 | 25 | /** 26 | * @var RequestInterface 27 | */ 28 | 29 | protected $request; 30 | 31 | /** 32 | * @var ResponseInterface 33 | */ 34 | 35 | protected $response; 36 | 37 | /** 38 | * RequestResponseStrategy constructor. 39 | * 40 | * @param RequestInterface $request 41 | * @param ResponseInterface $response 42 | */ 43 | 44 | public function __construct(RequestInterface $request, ResponseInterface $response) 45 | { 46 | $this->request = $request; 47 | $this->response = $response; 48 | } 49 | 50 | /** 51 | * @return RequestInterface 52 | */ 53 | 54 | public function getRequest() 55 | { 56 | return $this->request; 57 | } 58 | 59 | /** 60 | * @param RequestInterface $request 61 | * @return self 62 | */ 63 | 64 | public function setRequest(RequestInterface $request) 65 | { 66 | $this->request = $request; 67 | return $this; 68 | } 69 | 70 | /** 71 | * @return ResponseInterface 72 | */ 73 | 74 | public function getResponse() 75 | { 76 | return $this->response; 77 | } 78 | 79 | /** 80 | * @param ResponseInterface $response 81 | * @return self 82 | */ 83 | 84 | public function setResponse(ResponseInterface $response) 85 | { 86 | $this->response = $response; 87 | return $this; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Strategies/RequestJsonStrategy.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Strategies; 12 | 13 | use Psr\Http\Message\ResponseInterface; 14 | use Codeburner\Router\Exceptions\Http\HttpExceptionAbstract; 15 | use Codeburner\Router\Route; 16 | use RuntimeException; 17 | 18 | /** 19 | * The action will receive one Psr\Http\Message\RequestInterface object and one array with parameters 20 | * from pattern, and the return will create a json response, with right headers. 21 | * 22 | * @author Alex Rohleder 23 | */ 24 | 25 | class RequestJsonStrategy implements StrategyInterface 26 | { 27 | 28 | use RequestAwareTrait; 29 | 30 | /** 31 | * @inheritdoc 32 | * @throws RuntimeException 33 | * @return ResponseInterface 34 | */ 35 | 36 | public function call(Route $route) 37 | { 38 | try { 39 | $response = call_user_func($route->getAction(), $this->request, $route->getMergedParams()); 40 | 41 | if (is_array($response)) { 42 | $this->response->getBody()->write(json_encode($response)); 43 | $response = $this->response; 44 | } 45 | 46 | if ($response instanceof ResponseInterface) { 47 | return $response->withAddedHeader("content-type", "application/json"); 48 | } 49 | } catch (HttpExceptionAbstract $e) { 50 | return $e->getJsonResponse($this->response); 51 | } 52 | 53 | throw new RuntimeException("Unable to determine a json response from action returned value."); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Strategies/RequestResponseStrategy.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Strategies; 12 | 13 | use Psr\Http\Message\ResponseInterface; 14 | use Codeburner\Router\Exceptions\Http\HttpExceptionAbstract; 15 | use Codeburner\Router\Route; 16 | 17 | /** 18 | * The RequestResponseStrategy give an instance of Psr\Http\Message\RequestInterface, 19 | * Psr\Http\Message\Response interface and one array with parameters from pattern. 20 | * 21 | * @author Alex Rohleder 22 | */ 23 | 24 | class RequestResponseStrategy implements StrategyInterface 25 | { 26 | 27 | use RequestAwareTrait; 28 | 29 | /** 30 | * @inheritdoc 31 | * @return ResponseInterface 32 | */ 33 | 34 | public function call(Route $route) 35 | { 36 | try { 37 | $response = call_user_func($route->getAction(), $this->request, $this->response, $route->getMergedParams()); 38 | 39 | if ($response instanceof ResponseInterface) { 40 | return $response; 41 | } 42 | 43 | $this->response->getBody()->write((string) $response); 44 | return $this->response; 45 | } catch (HttpExceptionAbstract $e) { 46 | return $e->getResponse($this->response); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Strategies/StrategyInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Alex Rohleder 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Codeburner\Router\Strategies; 12 | 13 | /** 14 | * An interface that homogenizes all the dispatch strategies. 15 | * 16 | * @author Alex Rohleder 17 | */ 18 | 19 | interface StrategyInterface 20 | { 21 | 22 | /** 23 | * Dispatch the matched route action. 24 | * 25 | * @param \Codeburner\Router\Route $route 26 | * @return mixed The response of request. 27 | */ 28 | 29 | public function call(\Codeburner\Router\Route $route); 30 | 31 | } 32 | --------------------------------------------------------------------------------