This commit is contained in:
2024-07-03 14:41:15 +03:00
commit ec69208f05
1892 changed files with 181728 additions and 0 deletions

3
vendor/craft-group/phroute/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
vendor
composer.lock
.idea

17
vendor/craft-group/phroute/.travis.yml vendored Normal file
View File

@ -0,0 +1,17 @@
language: php
sudo: false
php:
- 5.6
- 7.0
- 7.1
before_script:
- composer install --dev
script:
- mkdir -p build/logs
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
after_script:
- php vendor/bin/coveralls -v

31
vendor/craft-group/phroute/LICENSE vendored Normal file
View File

@ -0,0 +1,31 @@
Copyright (c) 2013 by Nikita Popov.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

592
vendor/craft-group/phroute/README.md vendored Normal file
View File

@ -0,0 +1,592 @@
PHRoute - Fast request router for PHP
=======================================
[![Build Status](https://travis-ci.org/mrjgreen/phroute.svg)](https://travis-ci.org/mrjgreen/phroute)
[![Coverage Status](https://coveralls.io/repos/github/mrjgreen/phroute/badge.svg)](https://coveralls.io/github/mrjgreen/phroute)
[![Latest Stable Version](https://poser.pugx.org/phroute/phroute/v/stable)](https://packagist.org/packages/phroute/phroute)
[![License](https://poser.pugx.org/phroute/phroute/license)](https://packagist.org/packages/phroute/phroute)
[![Total Downloads](https://poser.pugx.org/phroute/phroute/downloads)](https://packagist.org/packages/phroute/phroute)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/92fb42d3-5254-4b3e-8a84-69d535941465/mini.png)](https://insight.sensiolabs.com/projects/92fb42d3-5254-4b3e-8a84-69d535941465)
## This library provides a fast implementation of a regular expression based router.
* [Super fast](#performance)
* [Route parameters and optional route parameters](#defining-routes)
* [Dependency Injection Resolving (Integrates easily with 3rd parties eg. Orno/Di)](#dependency-injection)
* [Named routes and reverse routing](#named-routes-for-reverse-routing)
* [Restful controller routing](#controllers)
* [Route filters and filter groups](#filters)
* [Route prefix groups](#prefix-groups)
### Credit to nikic/FastRoute.
While the bulk of the library and extensive unit tests are my own, credit for the regex matching core implementation and benchmarking goes to [nikic](https://github.com/nikic/FastRoute). Please go and read nikic's
[blog post explaining how the implementation works and why it's fast.](http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html)
Many modifications to the core have been made to suit the new library wrapper, and additional features added such as optional route parameters and reverse routing etc, but please head over and checkout nikic's library to see the origins of the core and how it works.
Installation
------------
Install via composer
```
composer require phroute/phroute
```
Usage
-----
### Example
~~~PHP
$router->get('/example', function(){
return 'This route responds to requests with the GET method at the path /example';
});
$router->post('/example/{id}', function($id){
return 'This route responds to requests with the POST method at the path /example/1234. It passes in the parameter as a function argument.';
});
$router->any('/example', function(){
return 'This route responds to any method (POST, GET, DELETE, OPTIONS, HEAD etc...) at the path /example';
});
~~~
### Defining routes
~~~PHP
use Phroute\Phroute\RouteCollector;
$router = new RouteCollector();
$router->get($route, $handler); # match only get requests
$router->post($route, $handler); # match only post requests
$router->delete($route, $handler); # match only delete requests
$router->any($route, $handler); # match any request method
etc...
~~~
> These helper methods are wrappers around `addRoute($method, $route, $handler)`
This method accepts the HTTP method the route must match, the route pattern and a callable handler, which can be a closure, function name or `['ClassName', 'method']` pair.
The methods also accept an additional parameter which is an array of middlewares: currently filters `before` and `after`, and route prefixing with `prefix` are supported. See the sections on Filters and Prefixes for more info and examples.
By default a route pattern syntax is used where `{foo}` specifies a placeholder with name `foo`
and matching the string `[^/]+`. To adjust the pattern the placeholder matches, you can specify
a custom pattern by writing `{bar:[0-9]+}`. However, it is also possible to adjust the pattern
syntax by passing a custom route parser to the router at construction.
```php
$router->any('/example', function(){
return 'This route responds to any method (POST, GET, DELETE etc...) at the URI /example';
});
// or '/page/{id:i}' (see shortcuts)
$router->post('/page/{id:\d+}', function($id){
// $id contains the url paramter
return 'This route responds to the post method at the URI /page/{param} where param is at least one number';
});
$router->any('/', function(){
return 'This responds to the default route';
});
// Lazy load autoloaded route handling classes using strings for classnames
// Calls the Controllers\User::displayUser($id) method with {id} parameter as an argument
$router->any('/users/{id}', ['Controllers\User','displayUser']);
// Optional Parameters
// simply add a '?' after the route name to make the parameter optional
// NB. be sure to add a default value for the function argument
$router->get('/user/{id}?', function($id = null) {
return 'second';
});
# NB. You can cache the return value from $router->getData() so you don't have to create the routes each request - massive speed gains
$dispatcher = new Phroute\Phroute\Dispatcher($router->getData());
$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
// Print out the value returned from the dispatched function
echo $response;
```
### Regex Shortcuts
```
:i => :/d+ # numbers only
:a => :[a-zA-Z0-9]+ # alphanumeric
:c => :[a-zA-Z0-9+_\-\.]+ # alnumnumeric and + _ - . characters
:h => :[a-fA-F0-9]+ # hex
use in routes:
'/user/{name:i}'
'/user/{name:a}'
```
###Named Routes for Reverse Routing
Pass in an array as the first argument, where the first item is your route and the second item is a name with which to reference it later.
```php
$router->get(['/user/{name}', 'username'], function($name){
return 'Hello ' . $name;
})
->get(['/page/{slug}/{id:\d+}', 'page'], function($id){
return 'You must be authenticated to see this page: ' . $id;
});
// Use the routename and pass in any route parameters to reverse engineer an existing route path
// If you change your route path above, you won't need to go through your code updating any links/references to that route
$router->route('username', 'joe');
// string(9) '/user/joe'
$router->route('page', ['intro', 456]);
// string(15) '/page/intro/456'
```
###Filters
```php
$router->filter('statsStart', function(){
setPageStartTime(microtime(true));
});
$router->filter('statsComplete', function(){
var_dump('Page load time: ' . (microtime(true) - getPageStartTime()));
});
$router->get('/user/{name}', function($name){
return 'Hello ' . $name;
}, ['before' => 'statsStart', 'after' => 'statsComplete']);
```
###Filter Groups
Wrap multiple routes in a route group to apply that filter to every route defined within. You can nest route groups if required.
```php
// Any thing other than null returned from a filter will prevent the route handler from being dispatched
$router->filter('auth', function(){
if(!isset($_SESSION['user']))
{
header('Location: /login');
return false;
}
});
$router->group(['before' => 'auth'], function($router){
$router->get('/user/{name}', function($name){
return 'Hello ' . $name;
})
->get('/page/{id:\d+}', function($id){
return 'You must be authenticated to see this page: ' . $id;
});
});
```
###Prefix Groups
```php
// You can combine a prefix with a filter, eg. `['prefix' => 'admin', 'before' => 'auth']`
$router->group(['prefix' => 'admin'], function($router){
$router->get('pages', function(){
return 'page management';
});
$router->get('products', function(){
return 'product management';
});
$router->get('orders', function(){
return 'order management';
});
});
```
###Controllers
```php
namespace MyApp;
class Test {
public function anyIndex()
{
return 'This is the default page and will respond to /controller and /controller/index';
}
/**
* One required paramter and one optional parameter
*/
public function anyTest($param, $param2 = 'default')
{
return 'This will respond to /controller/test/{param}/{param2}? with any method';
}
public function getTest()
{
return 'This will respond to /controller/test with only a GET method';
}
public function postTest()
{
return 'This will respond to /controller/test with only a POST method';
}
public function putTest()
{
return 'This will respond to /controller/test with only a PUT method';
}
public function deleteTest()
{
return 'This will respond to /controller/test with only a DELETE method';
}
}
$router->controller('/controller', 'MyApp\\Test');
// Controller with associated filter
$router->controller('/controller', 'MyApp\\Test', ['before' => 'auth']);
```
### Dispatching a URI
A URI is dispatched by calling the `dispatch()` method of the created dispatcher. This method
accepts the HTTP method and a URI. Getting those two bits of information (and normalizing them
appropriately) is your job - this library is not bound to the PHP web SAPIs.
$response = (new Phroute\Phroute\Dispatcher($router))
->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
The `dispatch()` method will call the matched route, or if no matches, throw one of the exceptions below:
# Route not found
Phroute\Phroute\Exception\HttpRouteNotFoundException;
# Route found, but method not allowed
Phroute\Phroute\Exception\HttpMethodNotAllowedException;
> **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the
`Allow:` header to detail available methods for the requested resource.
This information can be obtained from the thrown exception's message content:
which will look like: `"Allow: HEAD, GET, POST"` etc... depending on the methods you have set
You should catch the exception and use this to send a header to the client: `header($e->getMessage());`
###Dependency Injection
Defining your own dependency resolver is simple and easy. The router will attempt to resolve filters,
and route handlers via the dependency resolver.
The example below shows how you can define your own resolver to integrate with orno/di,
but pimple/pimple or others will work just as well.
~~~PHP
use Orno\Di\Container;
use Phroute\Phroute\HandlerResolverInterface;
class RouterResolver implements HandlerResolverInterface
{
private $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function resolve($handler)
{
/*
* Only attempt resolve uninstantiated objects which will be in the form:
*
* $handler = ['App\Controllers\Home', 'method'];
*/
if(is_array($handler) and is_string($handler[0]))
{
$handler[0] = $this->container[$handler[0]];
}
return $handler;
}
}
~~~
When you create your dispatcher:
~~~PHP
$appContainer = new Orno\Di;
// Attach your controllers as normal
// $appContainer->add('App\Controllers\Home')
$resolver = new RouterResolver($appContainer);
$response = (new Phroute\Phroute\Dispatcher($router, $resolver))->dispatch($requestMethod, $requestUri);
~~~
### A Note on HEAD Requests
The HTTP spec requires servers to [support both GET and HEAD methods][2616-511]:
> The methods GET and HEAD MUST be supported by all general-purpose servers
To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an
available GET route for a given resource. The PHP web SAPI transparently removes the entity body
from HEAD responses so this behavior has no effect on the vast majority of users.
However, implementors using Phroute outside the web SAPI environment (e.g. a custom server) MUST
NOT send entity bodies generated in response to HEAD requests. If you are a non-SAPI user this is
*your responsibility*; Phroute has no purview to prevent you from breaking HTTP in such cases.
Finally, note that applications MAY always specify their own HEAD method route for a given
resource to bypass this behavior entirely.
### Performance
Performed on a machine with :
* Processor 2.3 GHz Intel Core i7
* Memory 8 GB 1600 MHz DDR3
####Phroute
This test is to illustrate, in part, the efficiency of the lightweight routing-core, but mostly the lack of degradation of matching speed as the number of routes grows, as compared to conventional libraries.
##### With 10 routes, matching 1st route (best case)
~~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/
Finished 1000 requests
Time taken for tests: 3.062 seconds
Requests per second: 326.60 [#/sec] (mean)
Time per request: 306.181 [ms] (mean)
Time per request: 3.062 [ms] (mean, across all concurrent requests)
Transfer rate: 37.32 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 306
66% 307
75% 307
80% 308
90% 309
95% 309
98% 310
99% 310
100% 310 (longest request)
~~~~
##### With 10 routes, matching last route (worst case)
Note that the match is just as quick as against the first route
~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/thelastroute
Finished 1000 requests
Time taken for tests: 3.079 seconds
Requests per second: 324.80 [#/sec] (mean)
Time per request: 307.880 [ms] (mean)
Time per request: 3.079 [ms] (mean, across all concurrent requests)
Transfer rate: 37.11 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 307
66% 308
75% 309
80% 309
90% 310
95% 311
98% 312
99% 312
100% 313 (longest request)
~~~
##### With 100 routes, matching last route (worst case)
~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/thelastroute
Finished 1000 requests
Time taken for tests: 3.195 seconds
Requests per second: 312.97 [#/sec] (mean)
Time per request: 319.515 [ms] (mean)
Time per request: 3.195 [ms] (mean, across all concurrent requests)
Transfer rate: 35.76 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 318
66% 319
75% 320
80% 320
90% 322
95% 323
98% 323
99% 324
100% 324 (longest request)
~~~
##### With 1000 routes, matching the last route (worst case)
~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/thelastroute
Finished 1000 requests
Time taken for tests: 4.497 seconds
Complete requests: 1000
Requests per second: 222.39 [#/sec] (mean)
Time per request: 449.668 [ms] (mean)
Time per request: 4.497 [ms] (mean, across all concurrent requests)
Transfer rate: 25.41 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 445
66% 447
75% 448
80% 449
90% 454
95% 456
98% 457
99% 458
100% 478 (longest request)
~~~
###For comparison, Laravel 4.0 routing core
Please note, this is no slight against laravel - it is based on a routing loop, which is why the performance worsens as the number of routes grows
##### With 10 routes, matching first route (best case)
~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:4968/
Finished 1000 requests
Time taken for tests: 13.366 seconds
Requests per second: 74.82 [#/sec] (mean)
Time per request: 1336.628 [ms] (mean)
Time per request: 13.366 [ms] (mean, across all concurrent requests)
Transfer rate: 8.55 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 1336
66% 1339
75% 1340
80% 1341
90% 1346
95% 1348
98% 1349
99% 1351
100% 1353 (longest request)
~~~
##### With 10 routes, matching last route (worst case)
~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:4968/thelastroute
Finished 1000 requests
Time taken for tests: 14.621 seconds
Requests per second: 68.39 [#/sec] (mean)
Time per request: 1462.117 [ms] (mean)
Time per request: 14.621 [ms] (mean, across all concurrent requests)
Transfer rate: 7.81 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 1461
66% 1465
75% 1469
80% 1472
90% 1476
95% 1479
98% 1480
99% 1482
100% 1484 (longest request)
~~~
##### With 100 routes, matching last route (worst case)
~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:4968/thelastroute
Finished 1000 requests
Time taken for tests: 31.254 seconds
Requests per second: 32.00 [#/sec] (mean)
Time per request: 3125.402 [ms] (mean)
Time per request: 31.254 [ms] (mean, across all concurrent requests)
Transfer rate: 3.66 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 3124
66% 3145
75% 3154
80% 3163
90% 3188
95% 3219
98% 3232
99% 3236
100% 3241 (longest request)
~~~
##### With 1000 routes, matching last route (worst case)
~~~
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:5740/thelastroute
Finished 1000 requests
Time taken for tests: 197.366 seconds
Requests per second: 5.07 [#/sec] (mean)
Time per request: 19736.598 [ms] (mean)
Time per request: 197.366 [ms] (mean, across all concurrent requests)
Transfer rate: 0.58 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 19736
66% 19802
75% 19827
80% 19855
90% 19898
95% 19918
98% 19945
99% 19960
100% 19975 (longest request)
~~~

View File

@ -0,0 +1,54 @@
<?php
include __DIR__ . '/../vendor/autoload.php';
$collector = new Phroute\Phroute\RouteCollector();
$collector->get('/test', function(){
});
$collector->get('/test2', function(){
});
$collector->get('/test3', function(){
});
$collector->get('/test1/{name}', function(){
});
$collector->get('/test2/{name2}', function(){
});
$collector->get('/test3/{name3}', function(){
});
$dispatcher = new Phroute\Phroute\Dispatcher($collector->getData());
$runTime = 10;
$time = microtime(true);
$count = 0;
$seconds = 0;
while($seconds < $runTime)
{
$count++;
$dispatcher->dispatch('GET', '/test2/joe');
if($time + 1 < microtime(true))
{
$time = microtime(true);
$seconds++;
echo $count . ' routes dispatched per second' . "\r";
$count = 0;
}
}
echo PHP_EOL;

View File

@ -0,0 +1,24 @@
{
"name": "craft-group/phroute",
"description": "Fast, fully featured restful request router for PHP",
"keywords": ["routing", "router"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Kavalar",
"email": "apuc06@mail.ru"
}
],
"autoload": {
"psr-4": {
"Phroute\\Phroute\\": "src/Phroute"
}
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"satooshi/php-coveralls": "^1.0",
"phpunit/phpunit": "^5.0"
}
}

View File

@ -0,0 +1,32 @@
<?php
include __DIR__ . '/../vendor/autoload.php';
use Phroute\Phroute\RouteCollector;
use Phroute\Phroute\Dispatcher;
$collector = new RouteCollector();
$USER_SESSION = false;
$collector->filter('auth', function() use(&$USER_SESSION){
if(!$USER_SESSION)
{
return "Nope! Must be authenticated";
}
});
$collector->group(array('before' => 'auth'), function(RouteCollector $collector){
$collector->get('/', function(){
return 'Hurrah! Home Page';
});
});
$dispatcher = new Dispatcher($collector->getData());
echo $dispatcher->dispatch('GET', '/'), "\n"; // Nope! Must be authenticated
$USER_SESSION = true;
echo $dispatcher->dispatch('GET', '/'), "\n"; // Hurrah! Home Page

View File

@ -0,0 +1,29 @@
<?php
include __DIR__ . '/../vendor/autoload.php';
use Phroute\Phroute\RouteCollector;
use Phroute\Phroute\Dispatcher;
$collector = new RouteCollector();
$collector->group(array('prefix' => 'admin'), function(RouteCollector $collector){
$collector->get('pages', function(){
return 'page management';
});
$collector->get('products', function(){
return 'product management';
});
$collector->get('orders', function(){
return 'order management';
});
});
$dispatcher = new Dispatcher($collector->getData());
echo $dispatcher->dispatch('GET', '/admin/pages'), "\n"; // page management
echo $dispatcher->dispatch('GET', '/admin/products'), "\n"; // product management
echo $dispatcher->dispatch('GET', '/admin/orders'), "\n"; // order management

View File

@ -0,0 +1,36 @@
<?php
include __DIR__ . '/../vendor/autoload.php';
use Phroute\Phroute\RouteCollector;
use Phroute\Phroute\Dispatcher;
$collector = new RouteCollector();
$collector->filter('auth', function(){
return "Nope!";
});
$collector->group(array('prefix' => 'admin'), function(RouteCollector $collector){
$collector->group(['before' => 'auth'], function(RouteCollector $collector){
$collector->get('pages', function(){
return 'page management';
});
$collector->get('products', function(){
return 'product management';
});
});
// Not inside auth group
$collector->get('orders', function(){
return 'Order management';
});
});
$dispatcher = new Dispatcher($collector->getData());
echo $dispatcher->dispatch('GET', '/admin/pages'), "\n"; // Nope!
echo $dispatcher->dispatch('GET', '/admin/products'), "\n"; // Nope!
echo $dispatcher->dispatch('GET', '/admin/orders'), "\n"; // order management

View File

@ -0,0 +1,27 @@
<?php
error_reporting(-1);
ini_set('display_errors', 1);
include __DIR__ . '/../vendor/autoload.php';
use Phroute\Phroute\RouteCollector;
use Phroute\Phroute\Dispatcher;
$collector = new RouteCollector();
$collector->get('/', function(){
return 'Home Page';
});
$collector->post('products', function(){
return 'Create Product';
});
$collector->put('items/{id}', function($id){
return 'Amend Item ' . $id;
});
$dispatcher = new Dispatcher($collector->getData());
echo $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); // Home Page
//echo $dispatcher->dispatch('POST', '/products'), "\n"; // Create Product
//echo $dispatcher->dispatch('PUT', '/items/123'), "\n"; // Amend Item 123

25
vendor/craft-group/phroute/phpunit.xml vendored Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="true"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="Phroute Tests">
<directory>./test/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -0,0 +1,232 @@
<?php namespace Phroute\Phroute;
use Phroute\Phroute\Exception\HttpMethodNotAllowedException;
use Phroute\Phroute\Exception\HttpRouteNotFoundException;
class Dispatcher {
private $staticRouteMap;
private $variableRouteData;
private $filters;
private $handlerResolver;
public $matchedRoute;
/**
* Create a new route dispatcher.
*
* @param RouteDataInterface $data
* @param HandlerResolverInterface $resolver
*/
public function __construct(RouteDataInterface $data, HandlerResolverInterface $resolver = null)
{
$this->staticRouteMap = $data->getStaticRoutes();
$this->variableRouteData = $data->getVariableRoutes();
$this->filters = $data->getFilters();
if ($resolver === null)
{
$this->handlerResolver = new HandlerResolver();
}
else
{
$this->handlerResolver = $resolver;
}
}
/**
* Dispatch a route for the given HTTP Method / URI.
*
* @param $httpMethod
* @param $uri
* @return mixed|null
*/
public function dispatch($httpMethod, $uri)
{
list($handler, $filters, $vars) = $this->dispatchRoute($httpMethod, trim($uri, '/'));
list($beforeFilter, $afterFilter) = $this->parseFilters($filters);
if(($response = $this->dispatchFilters($beforeFilter)) !== null)
{
return $response;
}
$resolvedHandler = $this->handlerResolver->resolve($handler);
$response = call_user_func_array($resolvedHandler, $vars);
return $this->dispatchFilters($afterFilter, $response);
}
/**
* Dispatch a route filter.
*
* @param $filters
* @param null $response
* @return mixed|null
*/
private function dispatchFilters($filters, $response = null)
{
while($filter = array_shift($filters))
{
$handler = $this->handlerResolver->resolve($filter['closure']);
$params = array_merge([$response], $filter['params']);
if(($filteredResponse = call_user_func_array($handler, $params)) !== null)
{
return $filteredResponse;
}
}
return $response;
}
/**
* Normalise the array filters attached to the route and merge with any global filters.
*
* @param $filters
* @return array
*/
private function parseFilters($filters)
{
$beforeFilter = array();
$afterFilter = array();
if(isset($filters[Route::BEFORE]))
{
foreach ($filters[Route::BEFORE] as $filter){
if(array_key_exists($filter['name'], $this->filters)){
$beforeFilter[] = array_merge($filter, ['closure' => $this->filters[$filter['name']]]);
}
}
//$beforeFilter = array_intersect_key($this->filters, array_flip((array) $filters[Route::BEFORE]));
}
if(isset($filters[Route::AFTER]))
{
foreach ($filters[Route::AFTER] as $filter){
if(array_key_exists($filter['name'], $this->filters)){
$afterFilter[] = array_merge($filter, ['closure' => $this->filters[$filter['name']]]);
}
}
//$afterFilter = array_intersect_key($this->filters, array_flip((array) $filters[Route::AFTER]));
}
return array($beforeFilter, $afterFilter);
}
/**
* Perform the route dispatching. Check static routes first followed by variable routes.
*
* @param $httpMethod
* @param $uri
* @throws Exception\HttpRouteNotFoundException
*/
private function dispatchRoute($httpMethod, $uri)
{
if (isset($this->staticRouteMap[$uri]))
{
return $this->dispatchStaticRoute($httpMethod, $uri);
}
return $this->dispatchVariableRoute($httpMethod, $uri);
}
/**
* Handle the dispatching of static routes.
*
* @param $httpMethod
* @param $uri
* @return mixed
* @throws Exception\HttpMethodNotAllowedException
*/
private function dispatchStaticRoute($httpMethod, $uri)
{
$routes = $this->staticRouteMap[$uri];
if (!isset($routes[$httpMethod]))
{
$httpMethod = $this->checkFallbacks($routes, $httpMethod);
}
return $routes[$httpMethod];
}
/**
* Check fallback routes: HEAD for GET requests followed by the ANY attachment.
*
* @param $routes
* @param $httpMethod
* @throws Exception\HttpMethodNotAllowedException
*/
private function checkFallbacks($routes, $httpMethod)
{
$additional = array(Route::ANY);
if($httpMethod === Route::HEAD)
{
$additional[] = Route::GET;
}
foreach($additional as $method)
{
if(isset($routes[$method]))
{
return $method;
}
}
$this->matchedRoute = $routes;
throw new HttpMethodNotAllowedException('Allow: ' . implode(', ', array_keys($routes)));
}
/**
* Handle the dispatching of variable routes.
*
* @param $httpMethod
* @param $uri
* @throws Exception\HttpMethodNotAllowedException
* @throws Exception\HttpRouteNotFoundException
*/
private function dispatchVariableRoute($httpMethod, $uri)
{
foreach ($this->variableRouteData as $data)
{
if (!preg_match($data['regex'], $uri, $matches))
{
continue;
}
$count = count($matches);
while(!isset($data['routeMap'][$count++]));
$routes = $data['routeMap'][$count - 1];
if (!isset($routes[$httpMethod]))
{
$httpMethod = $this->checkFallbacks($routes, $httpMethod);
}
foreach (array_values($routes[$httpMethod][2]) as $i => $varName)
{
if(!isset($matches[$i + 1]) || $matches[$i + 1] === '')
{
unset($routes[$httpMethod][2][$varName]);
}
else
{
$routes[$httpMethod][2][$varName] = $matches[$i + 1];
}
}
return $routes[$httpMethod];
}
throw new HttpRouteNotFoundException('Route ' . $uri . ' does not exist');
}
}

View File

@ -0,0 +1,4 @@
<?php namespace Phroute\Phroute\Exception;
class BadRouteException extends \LogicException {
}

View File

@ -0,0 +1,3 @@
<?php namespace Phroute\Phroute\Exception;
class HttpException extends \Exception {}

View File

@ -0,0 +1,3 @@
<?php namespace Phroute\Phroute\Exception;
class HttpMethodNotAllowedException extends HttpException {}

View File

@ -0,0 +1,4 @@
<?php namespace Phroute\Phroute\Exception;
class HttpRouteNotFoundException extends HttpException {}

View File

@ -0,0 +1,21 @@
<?php namespace Phroute\Phroute;
class HandlerResolver implements HandlerResolverInterface {
/**
* Create an instance of the given handler.
*
* @param $handler
* @return array
*/
public function resolve ($handler)
{
if(is_array($handler) && is_string($handler[0]))
{
$handler[0] = new $handler[0];
}
return $handler;
}
}

View File

@ -0,0 +1,13 @@
<?php namespace Phroute\Phroute;
interface HandlerResolverInterface {
/**
* Create an instance of the given handler.
*
* @param $handler
* @return array
*/
public function resolve($handler);
}

View File

@ -0,0 +1,33 @@
<?php namespace Phroute\Phroute;
class Route {
/**
* Constants for before and after filters
*/
const BEFORE = 'before';
const AFTER = 'after';
const PREFIX = 'prefix';
/**
* Constants for common HTTP methods
*/
const ANY = 'ANY';
const GET = 'GET';
const HEAD = 'HEAD';
const POST = 'POST';
const PUT = 'PUT';
const PATCH = 'PATCH';
const DELETE = 'DELETE';
const OPTIONS = 'OPTIONS';
}

View File

@ -0,0 +1,472 @@
<?php namespace Phroute\Phroute;
use ReflectionClass;
use ReflectionMethod;
use Phroute\Phroute\Exception\BadRouteException;
/**
* Class RouteCollector
* @package Phroute\Phroute
*/
class RouteCollector implements RouteDataProviderInterface {
/**
*
*/
const DEFAULT_CONTROLLER_ROUTE = 'index';
/**
*
*/
const APPROX_CHUNK_SIZE = 10;
/**
* @var RouteParser
*/
private $routeParser;
/**
* @var array
*/
private $filters = [];
/**
* @var array
*/
private $staticRoutes = [];
/**
* @var array
*/
private $regexToRoutesMap = [];
/**
* @var array
*/
private $reverse = [];
/**
* @var array
*/
private $globalFilters = [];
/**
* @var
*/
private $globalRoutePrefix;
/**
* @param RouteParser $routeParser
*/
public function __construct(RouteParser $routeParser = null) {
$this->routeParser = $routeParser ?: new RouteParser();
}
/**
* @param $name
* @return bool
*/
public function hasRoute($name) {
return isset($this->reverse[$name]);
}
/**
* @param $name
* @param array $args
* @return string
*/
public function route($name, array $args = null)
{
$url = [];
$replacements = is_null($args) ? [] : array_values($args);
$variable = 0;
foreach($this->reverse[$name] as $part)
{
if(!$part['variable'])
{
$url[] = $part['value'];
}
elseif(isset($replacements[$variable]))
{
if($part['optional'])
{
$url[] = '/';
}
$url[] = $replacements[$variable++];
}
elseif(!$part['optional'])
{
throw new BadRouteException("Expecting route variable '{$part['name']}'");
}
}
return implode('', $url);
}
/**
* @param $httpMethod
* @param $route
* @param $handler
* @param array $filters
* @return $this
*/
public function addRoute($httpMethod, $route, $handler, array $filters = []) {
if(is_array($route))
{
list($route, $name) = $route;
}
$route = $this->addPrefix($this->trim($route));
list($routeData, $reverseData) = $this->routeParser->parse($route);
if(isset($name))
{
$this->reverse[$name] = $reverseData;
}
if(array_key_exists(Route::BEFORE, $filters)){
$filters = [Route::BEFORE => [['name' => $filters[Route::BEFORE], 'params' => $filters['params']]]];
}
if(array_key_exists(Route::AFTER, $filters)){
$filters = [Route::AFTER => [['name' => $filters[Route::AFTER], 'params' => $filters['params']]]];
}
$filters = array_merge_recursive($this->globalFilters, $filters);
isset($routeData[1]) ?
$this->addVariableRoute($httpMethod, $routeData, $handler, $filters) :
$this->addStaticRoute($httpMethod, $routeData, $handler, $filters);
return $this;
}
/**
* @param $httpMethod
* @param $routeData
* @param $handler
* @param $filters
*/
private function addStaticRoute($httpMethod, $routeData, $handler, $filters)
{
$routeStr = $routeData[0];
if (isset($this->staticRoutes[$routeStr][$httpMethod]))
{
throw new BadRouteException("Cannot register two routes matching '$routeStr' for method '$httpMethod'");
}
foreach ($this->regexToRoutesMap as $regex => $routes) {
if (isset($routes[$httpMethod]) && preg_match('~^' . $regex . '$~', $routeStr))
{
throw new BadRouteException("Static route '$routeStr' is shadowed by previously defined variable route '$regex' for method '$httpMethod'");
}
}
$this->staticRoutes[$routeStr][$httpMethod] = array($handler, $filters, []);
}
/**
* @param $httpMethod
* @param $routeData
* @param $handler
* @param $filters
*/
private function addVariableRoute($httpMethod, $routeData, $handler, $filters)
{
list($regex, $variables) = $routeData;
if (isset($this->regexToRoutesMap[$regex][$httpMethod]))
{
throw new BadRouteException("Cannot register two routes matching '$regex' for method '$httpMethod'");
}
$this->regexToRoutesMap[$regex][$httpMethod] = [$handler, $filters, $variables];
}
/**
* @param array $filters
* @param \Closure $callback
*/
public function group(array $filters, \Closure $callback)
{
if(array_key_exists(Route::BEFORE, $filters)){
$filters = [Route::BEFORE => [['name' => $filters[Route::BEFORE], 'params' => isset($filters['params']) ? $filters['params'] : []]]];
}
if(array_key_exists(Route::AFTER, $filters)){
$filters = [Route::AFTER => [['name' => $filters[Route::AFTER], 'params' => isset($filters['params']) ? $filters['params'] : []]]];
}
$oldGlobalFilters = $this->globalFilters;
$oldGlobalPrefix = $this->globalRoutePrefix;
$this->globalFilters = array_merge_recursive($this->globalFilters, array_intersect_key($filters, [Route::AFTER => 1, Route::BEFORE => 1]));
$newPrefix = isset($filters[Route::PREFIX]) ? $this->trim($filters[Route::PREFIX]) : null;
$this->globalRoutePrefix = $this->addPrefix($newPrefix);
$callback($this);
$this->globalFilters = $oldGlobalFilters;
$this->globalRoutePrefix = $oldGlobalPrefix;
}
private function addPrefix($route)
{
return $this->trim($this->trim($this->globalRoutePrefix) . '/' . $route);
}
/**
* @param $name
* @param $handler
*/
public function filter($name, $handler)
{
$this->filters[$name] = $handler;
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function get($route, $handler, array $filters = [])
{
return $this->addRoute(Route::GET, $route, $handler, $filters);
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function head($route, $handler, array $filters = [])
{
return $this->addRoute(Route::HEAD, $route, $handler, $filters);
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function post($route, $handler, array $filters = [])
{
return $this->addRoute(Route::POST, $route, $handler, $filters);
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function put($route, $handler, array $filters = [])
{
return $this->addRoute(Route::PUT, $route, $handler, $filters);
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function patch($route, $handler, array $filters = [])
{
return $this->addRoute(Route::PATCH, $route, $handler, $filters);
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function delete($route, $handler, array $filters = [])
{
return $this->addRoute(Route::DELETE, $route, $handler, $filters);
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function options($route, $handler, array $filters = [])
{
return $this->addRoute(Route::OPTIONS, $route, $handler, $filters);
}
/**
* @param $route
* @param $handler
* @param array $filters
* @return RouteCollector
*/
public function any($route, $handler, array $filters = [])
{
return $this->addRoute(Route::ANY, $route, $handler, $filters);
}
/**
* @param $route
* @param $classname
* @param array $filters
* @return $this
*/
public function controller($route, $classname, array $filters = [])
{
$reflection = new ReflectionClass($classname);
$validMethods = $this->getValidMethods();
$sep = $route === '/' ? '' : '/';
foreach($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
{
foreach($validMethods as $valid)
{
if(stripos($method->name, $valid) === 0)
{
$methodName = $this->camelCaseToDashed(substr($method->name, strlen($valid)));
$params = $this->buildControllerParameters($method);
if($methodName === self::DEFAULT_CONTROLLER_ROUTE)
{
$this->addRoute($valid, $route . $params, [$classname, $method->name], $filters);
}
$this->addRoute($valid, $route . $sep . $methodName . $params, [$classname, $method->name], $filters);
break;
}
}
}
return $this;
}
/**
* @param ReflectionMethod $method
* @return string
*/
private function buildControllerParameters(ReflectionMethod $method)
{
$params = '';
foreach($method->getParameters() as $param)
{
$params .= "/{" . $param->getName() . "}" . ($param->isOptional() ? '?' : '');
}
return $params;
}
/**
* @param $string
* @return string
*/
private function camelCaseToDashed($string)
{
return strtolower(preg_replace('/([A-Z])/', '-$1', lcfirst($string)));
}
/**
* @return array
*/
public function getValidMethods()
{
return [
Route::ANY,
Route::GET,
Route::POST,
Route::PUT,
Route::PATCH,
Route::DELETE,
Route::HEAD,
Route::OPTIONS,
];
}
/**
* @return RouteDataArray
*/
public function getData()
{
if (empty($this->regexToRoutesMap))
{
return new RouteDataArray($this->staticRoutes, [], $this->filters);
}
return new RouteDataArray($this->staticRoutes, $this->generateVariableRouteData(), $this->filters);
}
/**
* @param $route
* @return string
*/
private function trim($route)
{
if(is_null($route)) {
return null;
}
return trim($route, '/');
}
/**
* @return array
*/
private function generateVariableRouteData()
{
$chunkSize = $this->computeChunkSize(count($this->regexToRoutesMap));
$chunks = array_chunk($this->regexToRoutesMap, $chunkSize, true);
return array_map([$this, 'processChunk'], $chunks);
}
/**
* @param $count
* @return float
*/
private function computeChunkSize($count)
{
$numParts = max(1, round($count / self::APPROX_CHUNK_SIZE));
return ceil($count / $numParts);
}
/**
* @param $regexToRoutesMap
* @return array
*/
private function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$numGroups = 0;
foreach ($regexToRoutesMap as $regex => $routes) {
$firstRoute = reset($routes);
$numVariables = count($firstRoute[2]);
$numGroups = max($numGroups, $numVariables);
$regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
foreach ($routes as $httpMethod => $route) {
$routeMap[$numGroups + 1][$httpMethod] = $route;
}
$numGroups++;
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}

View File

@ -0,0 +1,57 @@
<?php namespace Phroute\Phroute;
class RouteDataArray implements RouteDataInterface {
/**
* @var array
*/
private $variableRoutes;
/**
* @var array
*/
private $staticRoutes;
/**
* @var array
*/
private $filters;
/**
* @param array $staticRoutes
* @param array $variableRoutes
* @param array $filters
*/
public function __construct(array $staticRoutes, array $variableRoutes, array $filters)
{
$this->staticRoutes = $staticRoutes;
$this->variableRoutes = $variableRoutes;
$this->filters = $filters;
}
/**
* @return array
*/
public function getStaticRoutes()
{
return $this->staticRoutes;
}
/**
* @return array
*/
public function getVariableRoutes()
{
return $this->variableRoutes;
}
/**
* @return mixed
*/
public function getFilters()
{
return $this->filters;
}
}

View File

@ -0,0 +1,24 @@
<?php namespace Phroute\Phroute;
/**
* Interface RouteDataInterface
* @package Phroute\Phroute
*/
interface RouteDataInterface {
/**
* @return array
*/
public function getStaticRoutes();
/**
* @return array
*/
public function getVariableRoutes();
/**
* @return array
*/
public function getFilters();
}

View File

@ -0,0 +1,14 @@
<?php namespace Phroute\Phroute;
/**
* Interface RouteDataProviderInterface
* @package Phroute\Phroute
*/
interface RouteDataProviderInterface {
/**
* @return mixed
*/
public function getData();
}

View File

@ -0,0 +1,207 @@
<?php namespace Phroute\Phroute;
use Phroute\Phroute\Exception\BadRouteException;
/**
* Parses routes of the following form:
*
* "/user/{name}/{id:[0-9]+}?"
*/
class RouteParser {
/**
* Search through the given route looking for dynamic portions.
*
* Using ~ as the regex delimiter.
*
* We start by looking for a literal '{' character followed by any amount of whitespace.
* The next portion inside the parentheses looks for a parameter name containing alphanumeric characters or underscore.
*
* After this we look for the ':\d+' and ':[0-9]+' style portion ending with a closing '}' character.
*
* Finally we look for an optional '?' which is used to signify an optional route.
*/
const VARIABLE_REGEX =
"~\{
\s* ([a-zA-Z0-9_]*) \s*
(?:
: \s* ([^{]+(?:\{.*?\})?)
)?
\}\??~x";
/**
* The default parameter character restriction (One or more characters that is not a '/').
*/
const DEFAULT_DISPATCH_REGEX = '[^/]+';
private $parts;
private $reverseParts;
private $partsCounter;
private $variables;
private $regexOffset;
/**
* Handy parameter type restrictions.
*
* @var array
*/
private $regexShortcuts = array(
':i}' => ':[0-9]+}',
':a}' => ':[0-9A-Za-z]+}',
':h}' => ':[0-9A-Fa-f]+}',
':c}' => ':[a-zA-Z0-9+_\-\.]+}'
);
/**
* Parse a route returning the correct data format to pass to the dispatch engine.
*
* @param $route
* @return array
*/
public function parse($route)
{
$this->reset();
$route = strtr($route, $this->regexShortcuts);
if (!$matches = $this->extractVariableRouteParts($route))
{
$reverse = array(
'variable' => false,
'value' => $route
);
return [[$route], array($reverse)];
}
foreach ($matches as $set) {
$this->staticParts($route, $set[0][1]);
$this->validateVariable($set[1][0]);
$regexPart = (isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX);
$this->regexOffset = $set[0][1] + strlen($set[0][0]);
$match = '(' . $regexPart . ')';
$isOptional = substr($set[0][0], -1) === '?';
if($isOptional)
{
$match = $this->makeOptional($match);
}
$this->reverseParts[$this->partsCounter] = array(
'variable' => true,
'optional' => $isOptional,
'name' => $set[1][0]
);
$this->parts[$this->partsCounter++] = $match;
}
$this->staticParts($route, strlen($route));
return [[implode('', $this->parts), $this->variables], array_values($this->reverseParts)];
}
/**
* Reset the parser ready for the next route.
*/
private function reset()
{
$this->parts = array();
$this->reverseParts = array();
$this->partsCounter = 0;
$this->variables = array();
$this->regexOffset = 0;
}
/**
* Return any variable route portions from the given route.
*
* @param $route
* @return mixed
*/
private function extractVariableRouteParts($route)
{
if(preg_match_all(self::VARIABLE_REGEX, $route, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER))
{
return $matches;
}
}
/**
* @param $route
* @param $nextOffset
*/
private function staticParts($route, $nextOffset)
{
$static = preg_split('~(/)~u', substr($route, $this->regexOffset, $nextOffset - $this->regexOffset), 0, PREG_SPLIT_DELIM_CAPTURE);
foreach($static as $staticPart)
{
if($staticPart)
{
$quotedPart = $this->quote($staticPart);
$this->parts[$this->partsCounter] = $quotedPart;
$this->reverseParts[$this->partsCounter] = array(
'variable' => false,
'value' => $staticPart
);
$this->partsCounter++;
}
}
}
/**
* @param $varName
*/
private function validateVariable($varName)
{
if (isset($this->variables[$varName]))
{
throw new BadRouteException("Cannot use the same placeholder '$varName' twice");
}
$this->variables[$varName] = $varName;
}
/**
* @param $match
* @return string
*/
private function makeOptional($match)
{
$previous = $this->partsCounter - 1;
if(isset($this->parts[$previous]) && $this->parts[$previous] === '/')
{
$this->partsCounter--;
$match = '(?:/' . $match . ')';
}
return $match . '?';
}
/**
* @param $part
* @return string
*/
private function quote($part)
{
return preg_quote($part, '~');
}
}

File diff suppressed because it is too large Load Diff