├── 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 | [](https://packagist.org/packages/codeburner/router)
4 | [](https://travis-ci.org/codeburnerframework/router)
5 | [](https://scrutinizer-ci.com/g/codeburnerframework/routing/?branch=master)
6 | [](https://scrutinizer-ci.com/g/codeburnerframework/routing/?branch=master)
7 | [](LICENSE)
8 |
9 | [](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 |
--------------------------------------------------------------------------------