From b9f237d76c05acfb3bcbfc52da6b640e0fbb108b Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Wed, 25 Mar 2020 19:49:05 +0430 Subject: [PATCH 01/52] change zendframework/zend-diactoros to laminas/laminas-diactoros --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 650874e..6e2fc07 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "php": ">=7.1", "ext-json": "*", "ext-mbstring": "*", - "zendframework/zend-diactoros": "^2.1" + "laminas/laminas-diactoros": "^2.2" }, "require-dev": { "phpunit/phpunit": "^7" From ef9b8bc62e17afad48b7fb404a19051abfe50bbe Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Wed, 25 Mar 2020 19:49:27 +0430 Subject: [PATCH 02/52] update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2e74e0b..fe27e8c 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ $router->dispatch(); ## HTTP Request and Request -PhpRouter uses [zend-diactoros](https://github.com/zendframework/zend-diactoros) package (v2) to provide [PSR-7](https://www.php-fig.org/psr/psr-7) request and response objects to your controllers and middleware. +PhpRouter uses [laminas-diactoros](https://github.com/laminas/laminas-diactoros/) (formerly known as [zend-diactoros](https://github.com/zendframework/zend-diactoros)) package (v2) to provide [PSR-7](https://www.php-fig.org/psr/psr-7) request and response objects to your controllers and middleware. ### Request @@ -564,7 +564,7 @@ try { } ``` -PhpRouter also throws the following exceptions: +PhpRouter throws the following exceptions: * `RouteNotFoundException` if PhpRouter cannot find any route that matches the user request. * `InvalidControllerException` if PhpRouter cannot invoke the controller. From 585d020c02371f7b48de11c32e00c70af3338129 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Wed, 25 Mar 2020 23:49:15 +0430 Subject: [PATCH 03/52] update README.md --- README.md | 83 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index fe27e8c..77e293c 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,47 @@ # PhpRouter -PhpRouter is a powerful, standalone, and very fast HTTP URL router for PHP projects. +PhpRouter is a powerful, lightweight, and very fast HTTP URL router for PHP projects. -Some of the supported features: +Some of the provided features: * Route parameters * Middleware * Route groups (URI prefix, namespace prefix, middleware, and domain) * Route names * PSR-7 requests and responses +* View module (A simple view layer) +* Multiple domains or subdomains (regex pattern) * Multiple controller types (class, closure, and function) * Predefined route parameter regex patterns -* Multiple domains or subdomains (regex pattern) * Custom HTTP methods * Request, response and router instance injection Current version requires PHP `v7.1` or newer versions. +## Contents +- [PhpRouter](#phprouter) + - [Contents](#contents) + - [Versions](#versions) + - [Installation](#installation) + - [Configuration](#configuration) + - [Documentation](#documentation) + - [Getting Started](#getting-started) + - [HTTP Methods](#http-methods) + - [Controllers](#controllers) + - [Route Parameters](#route-parameters) + - [HTTP Request and Request](#http-request-and-request) + - [Middleware](#middleware) + - [Domain and Sub-domain](#domain-and-sub-domain) + - [Route Groups](#route-groups) + - [URI Prefix](#uri-prefix) + - [Route Name](#route-name) + - [Current Route](#current-route) + - [Error Handling](#error-handling) + - [License](#license) + ## Versions +* **v5.x.x (LTS)** * **v4.x.x (LTS)** * v3.x.x (Unsupported) * ~~v2.x.x~~ (Unavailable) @@ -34,7 +57,7 @@ Current version requires PHP `v7.1` or newer versions. Install [Composer](https://getcomposer.org) and run following command in your project's root directory: ```bash -composer require miladrahimi/phprouter "4.*" +composer require miladrahimi/phprouter "5.*" ``` ## Configuration @@ -66,9 +89,11 @@ First of all, you need to configure your webserver to handle all the HTTP reques } ``` -## Getting Started +## Documentation + +### Getting Started -After applying the configurations mentioned above, you can start using PhpRouter in your entry point file (`index.php`) like this example: +It's so easy to work with PhpRouter! Just take a look at this example: ```php use MiladRahimi\PhpRouter\Router; @@ -76,13 +101,13 @@ use MiladRahimi\PhpRouter\Router; $router = new Router(); $router->get('/', function () { - return '

This is homepage!

'; + return 'This is homepage!'; }); $router->dispatch(); ``` -Chaining methods is also possible, take a look at this example: +And if you are a fan of method chaining: ```php use MiladRahimi\PhpRouter\Router; @@ -98,9 +123,9 @@ $router There are more examples available [here](https://github.com/miladrahimi/phprouter/tree/master/examples). -## HTTP Methods +### HTTP Methods -Here you can see how to declare different routes for different HTTP methods: +The following example illustrates how to declare different routes for different HTTP methods. There is also `any` method for handling a route with any HTTP method. ```php use MiladRahimi\PhpRouter\Router; @@ -129,7 +154,7 @@ $router ->dispatch(); ``` -You may want to use your custom HTTP methods, so take a look at this example: +Or you may want to use your custom HTTP methods, so take a look at this example: ```php use MiladRahimi\PhpRouter\Router; @@ -149,7 +174,7 @@ $router ->dispatch(); ``` -## Controllers +### Controllers PhpRouter supports plenty of controller types, just look at the following examples. @@ -170,7 +195,7 @@ $router->get('/function', 'func'); $router->dispatch(); ``` -And class controllers: +And here is a controller class: ```php use MiladRahimi\PhpRouter\Router; @@ -185,12 +210,12 @@ class Controller } } -$router->get('/method', 'Controller@method'); +$router->get('/', 'Controller@method'); $router->dispatch(); ``` -If your controller class has a namespace: +When your controller class has a namespace: ```php use App\Controllers\TheController; @@ -198,9 +223,9 @@ use MiladRahimi\PhpRouter\Router; $router = new Router(); -$router->get('/ns', 'App\Controllers\TheController@show'); +$router->get('/', 'App\Controllers\TheController@show'); // OR -$router->get('/ns', TheController::class . '@show'); +$router->get('/', TheController::class . '@show'); $router->dispatch(); ``` @@ -218,9 +243,9 @@ $router->get('/', 'TheController@show'); $router->dispatch(); ``` -## Route Parameters +### Route Parameters -A URL might have one or more variable parts like the id in a blog post URL. We call it the route parameter. You can catch them by controller parameters with the same names. +A URL might have one or more variable parts like the *id* in a blog post URL. We call it a route parameter. You can catch them by controller arguments with the same names. ```php use MiladRahimi\PhpRouter\Router; @@ -271,11 +296,11 @@ $router->get('/blog/post/{id}', function (int $id) { $router->dispatch(); ``` -## HTTP Request and Request +### HTTP Request and Request PhpRouter uses [laminas-diactoros](https://github.com/laminas/laminas-diactoros/) (formerly known as [zend-diactoros](https://github.com/zendframework/zend-diactoros)) package (v2) to provide [PSR-7](https://www.php-fig.org/psr/psr-7) request and response objects to your controllers and middleware. -### Request +#### Request You can catch the PSR-7 request object in your controllers like this example: @@ -311,7 +336,7 @@ $router->post('/posts', function (ServerRequest $request) { $router->dispatch(); ``` -### Response +#### Response The example below illustrates the built-in responses. @@ -347,7 +372,7 @@ $router $router->dispatch(); ``` -## Middleware +### Middleware PhpRouter supports middleware. You can use it for different purposes such as authentication, authorization, throttles and so forth. Middleware runs before controllers and it can check and manipulate the request and response. @@ -408,7 +433,7 @@ $router->dispatch(); Middleware can be implemented using closures but it doesn’t make scense to do so! -## Domain and Sub-domain +### Domain and Sub-domain Your application may serve different services on different domains or subdomains. In this case, you can specify the domain or subdomain for your routes. See this example: @@ -427,7 +452,7 @@ $router->get('/', 'Controller@method', [], '(.*).domain.com'); $router->dispatch(); ``` -## Route Groups +### Route Groups Application routes can be categorized into groups if they have common attributes like middleware, domain, or prefix. The following example shows how to group routes: @@ -466,7 +491,7 @@ $router->group($attributes, function (Router $router) { $router->dispatch(); ``` -## URI Prefix +### URI Prefix Your project might be in a subdirectory, or all of your routes might start with the same prefix. You can pass this prefix as the constructor like this example: @@ -488,7 +513,7 @@ $router->get('/product/{id}', function ($id) { $router->dispatch(); ``` -## Route Name +### Route Name You can define names for your routes and use them in your codes instead of the URLs. See this example: @@ -517,7 +542,7 @@ $router->name('home')->get('/', function (Router $router) { $router->dispatch(); ``` -## Current Route +### Current Route You might want to get information about the current route in your controller. This example shows how to get this information. @@ -539,7 +564,7 @@ $router->name('home')->get('/', function (Router $router) { $router->dispatch(); ``` -## Error Handling +### Error Handling Your application runs through the `Router::dispatch()` method. You should put it in a `try` block and catch exceptions that will be thrown by your application and PhpRouter. From 430a972d7e4f4e2e349ccc6b180a688da9cb72c5 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Thu, 26 Mar 2020 00:01:43 +0430 Subject: [PATCH 04/52] change zend to laminas --- README.md | 22 +++++++++++----------- examples/example-06/index.php | 2 +- examples/example-11/index.php | 6 +++--- examples/example-12/index.php | 8 ++++---- examples/example-13/index.php | 2 +- examples/example-14/index.php | 2 +- examples/example-18/index.php | 2 +- examples/example-19/index.php | 2 +- examples/example-20/index.php | 2 +- src/Router.php | 4 ++-- tests/MiddlewareTest.php | 2 +- tests/ResponseTest.php | 10 +++++----- tests/RoutingTest.php | 2 +- tests/Testing/StopperMiddleware.php | 2 +- 14 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 77e293c..645c1fc 100644 --- a/README.md +++ b/README.md @@ -306,9 +306,9 @@ You can catch the PSR-7 request object in your controllers like this example: ```php use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\ServerRequest; -use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\ServerRequest; +use Laminas\Diactoros\Response\EmptyResponse; +use Laminas\Diactoros\Response\JsonResponse; $router = new Router(); @@ -342,10 +342,10 @@ The example below illustrates the built-in responses. ```php use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\HtmlResponse; -use Zend\Diactoros\Response\JsonResponse; -use Zend\Diactoros\Response\TextResponse; +use Laminas\Diactoros\Response\EmptyResponse; +use Laminas\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\TextResponse; $router = new Router(); @@ -408,7 +408,7 @@ See the following example. In the implemented middelware, if there is an `Author use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Middleware; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\JsonResponse; class AuthMiddleware implements Middleware { @@ -519,7 +519,7 @@ You can define names for your routes and use them in your codes instead of the U ```php use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\JsonResponse; $router = new Router(); @@ -548,7 +548,7 @@ You might want to get information about the current route in your controller. Th ```php use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\JsonResponse; $router = new Router(); @@ -571,7 +571,7 @@ Your application runs through the `Router::dispatch()` method. You should put it ```php use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; -use Zend\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\HtmlResponse; $router = new Router(); diff --git a/examples/example-06/index.php b/examples/example-06/index.php index 2c9066a..7e29027 100644 --- a/examples/example-06/index.php +++ b/examples/example-06/index.php @@ -3,7 +3,7 @@ require('../../vendor/autoload.php'); use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\HtmlResponse; $router = new Router(); diff --git a/examples/example-11/index.php b/examples/example-11/index.php index 5fb4600..bbda684 100644 --- a/examples/example-11/index.php +++ b/examples/example-11/index.php @@ -4,9 +4,9 @@ use MiladRahimi\PhpRouter\Examples\Shared\PostModel; use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\ServerRequest; -use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\ServerRequest; +use Laminas\Diactoros\Response\EmptyResponse; +use Laminas\Diactoros\Response\JsonResponse; $router = new Router(); diff --git a/examples/example-12/index.php b/examples/example-12/index.php index 775fe4d..907b4fe 100644 --- a/examples/example-12/index.php +++ b/examples/example-12/index.php @@ -3,10 +3,10 @@ require('../../vendor/autoload.php'); use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\HtmlResponse; -use Zend\Diactoros\Response\JsonResponse; -use Zend\Diactoros\Response\TextResponse; +use Laminas\Diactoros\Response\EmptyResponse; +use Laminas\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\TextResponse; $router = new Router(); diff --git a/examples/example-13/index.php b/examples/example-13/index.php index fa0657e..9aaf027 100644 --- a/examples/example-13/index.php +++ b/examples/example-13/index.php @@ -3,7 +3,7 @@ require('../../vendor/autoload.php'); use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\RedirectResponse; +use Laminas\Diactoros\Response\RedirectResponse; $router = new Router(); diff --git a/examples/example-14/index.php b/examples/example-14/index.php index cba4afb..58db27a 100644 --- a/examples/example-14/index.php +++ b/examples/example-14/index.php @@ -5,7 +5,7 @@ use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Middleware; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\JsonResponse; class AuthMiddleware implements Middleware { diff --git a/examples/example-18/index.php b/examples/example-18/index.php index b4cdf89..2ac6792 100644 --- a/examples/example-18/index.php +++ b/examples/example-18/index.php @@ -3,7 +3,7 @@ require('../../vendor/autoload.php'); use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\JsonResponse; $router = new Router(); diff --git a/examples/example-19/index.php b/examples/example-19/index.php index 4107bd8..e30ffb2 100644 --- a/examples/example-19/index.php +++ b/examples/example-19/index.php @@ -3,7 +3,7 @@ require('../../vendor/autoload.php'); use MiladRahimi\PhpRouter\Router; -use Zend\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\JsonResponse; $router = new Router(); diff --git a/examples/example-20/index.php b/examples/example-20/index.php index ccc2a04..a065783 100644 --- a/examples/example-20/index.php +++ b/examples/example-20/index.php @@ -4,7 +4,7 @@ use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; -use Zend\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\HtmlResponse; $router = new Router(); diff --git a/src/Router.php b/src/Router.php index 66ef447..cf6ff1d 100644 --- a/src/Router.php +++ b/src/Router.php @@ -20,8 +20,8 @@ use ReflectionMethod; use ReflectionParameter; use Throwable; -use Zend\Diactoros\ServerRequest; -use Zend\Diactoros\ServerRequestFactory; +use Laminas\Diactoros\ServerRequest; +use Laminas\Diactoros\ServerRequestFactory; /** * Class Router diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index e28cf88..7cb4490 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -7,7 +7,7 @@ use MiladRahimi\PhpRouter\Tests\Testing\SampleMiddleware; use MiladRahimi\PhpRouter\Tests\Testing\StopperMiddleware; use Throwable; -use Zend\Diactoros\ServerRequest; +use Laminas\Diactoros\ServerRequest; class MiddlewareTest extends TestCase { diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 0af269e..e646ed0 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -3,11 +3,11 @@ namespace MiladRahimi\PhpRouter\Tests; use Throwable; -use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\HtmlResponse; -use Zend\Diactoros\Response\JsonResponse; -use Zend\Diactoros\Response\RedirectResponse; -use Zend\Diactoros\Response\TextResponse; +use Laminas\Diactoros\Response\EmptyResponse; +use Laminas\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Response\RedirectResponse; +use Laminas\Diactoros\Response\TextResponse; class ResponseTest extends TestCase { diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 31193fa..e2ed960 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -8,7 +8,7 @@ use MiladRahimi\PhpRouter\Router; use Psr\Http\Message\ServerRequestInterface; use Throwable; -use Zend\Diactoros\ServerRequest; +use Laminas\Diactoros\ServerRequest; class RoutingTest extends TestCase { diff --git a/tests/Testing/StopperMiddleware.php b/tests/Testing/StopperMiddleware.php index 55b12cb..e511041 100644 --- a/tests/Testing/StopperMiddleware.php +++ b/tests/Testing/StopperMiddleware.php @@ -5,7 +5,7 @@ use Closure; use MiladRahimi\PhpRouter\Middleware; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response\TextResponse; +use Laminas\Diactoros\Response\TextResponse; class StopperMiddleware implements Middleware { From 29493fe6151001de6d4456d5af3dda67f8e7f277 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Fri, 27 Mar 2020 21:10:40 +0430 Subject: [PATCH 05/52] remove prefix from constructor --- src/Router.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Router.php b/src/Router.php index cf6ff1d..9cc8003 100644 --- a/src/Router.php +++ b/src/Router.php @@ -110,12 +110,10 @@ class Router /** * Router constructor. * - * @param string $uriPrefix * @param string $namespacePrefix */ - public function __construct(string $uriPrefix = '', string $namespacePrefix = '') + public function __construct(string $namespacePrefix = '') { - $this->prefix = $uriPrefix; $this->namespace = $namespacePrefix; } From c5751e97208e512399b1a9421aa267b22e04a1c6 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Fri, 27 Mar 2020 21:17:27 +0430 Subject: [PATCH 06/52] update tests --- src/Router.php | 2 +- tests/RoutingTest.php | 16 +--------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Router.php b/src/Router.php index 9cc8003..c25f035 100644 --- a/src/Router.php +++ b/src/Router.php @@ -87,7 +87,7 @@ class Router private $prefix; /** - * Controller namespace prefix for next routes + * Controller namespace (prefix) for next routes * * @var string */ diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index e2ed960..f454b4d 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -168,20 +168,6 @@ public function test_multiple_http_methods() $this->assertEquals('Post', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_initial_prefix() - { - $this->mockRequest(HttpMethods::POST, 'http://example.com/app/page'); - - $router = $this->router('/app') - ->post('/page', $this->OkController()) - ->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - /** * @throws Throwable */ @@ -426,7 +412,7 @@ public function test_with_preserved_namespaced_controller() { $namespace = 'MiladRahimi\PhpRouter\Tests\Testing'; - $router = $this->router('', $namespace) + $router = $this->router($namespace) ->get('/', 'SampleController@home') ->dispatch(); From a648666c35b96b5fde4d744dd2f2712b51e6b80c Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 30 Mar 2020 13:48:48 +0430 Subject: [PATCH 07/52] remove middleware and domain from route definition method --- README.md | 25 +--------- src/Router.php | 98 +++++++--------------------------------- src/Values/Route.php | 6 +-- tests/GroupingTest.php | 48 -------------------- tests/MiddlewareTest.php | 41 +++++++++-------- tests/NamingTest.php | 31 ++----------- tests/RoutingTest.php | 36 ++------------- tests/TestCase.php | 5 +- tests/UrlTest.php | 23 ++++------ 9 files changed, 59 insertions(+), 254 deletions(-) diff --git a/README.md b/README.md index 645c1fc..1a1832b 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ Current version requires PHP `v7.1` or newer versions. - [Middleware](#middleware) - [Domain and Sub-domain](#domain-and-sub-domain) - [Route Groups](#route-groups) - - [URI Prefix](#uri-prefix) - [Route Name](#route-name) - [Current Route](#current-route) - [Error Handling](#error-handling) @@ -280,7 +279,7 @@ $router->get('/post/{pid}/comment/{cid}', function ($pid, $cid) { $router->dispatch(); ``` -In default, route parameters can be any value, but you can define regex patterns for each of them. +In default, route parameters can hold any value, but you can define regex patterns for each of them. ```php use MiladRahimi\PhpRouter\Router; @@ -491,28 +490,6 @@ $router->group($attributes, function (Router $router) { $router->dispatch(); ``` -### URI Prefix - -Your project might be in a subdirectory, or all of your routes might start with the same prefix. You can pass this prefix as the constructor like this example: - -```php -use MiladRahimi\PhpRouter\Router; - -$router = new Router('/shop'); - -// URI: /shop/about -$router->get('/about', function () { - return 'About the shop.'; -}); - -// URI: /shop/product/{id} -$router->get('/product/{id}', function ($id) { - return 'A product.'; -}); - -$router->dispatch(); -``` - ### Route Name You can define names for your routes and use them in your codes instead of the URLs. See this example: diff --git a/src/Router.php b/src/Router.php index c25f035..cc84e63 100644 --- a/src/Router.php +++ b/src/Router.php @@ -35,7 +35,7 @@ class Router /** * List of defined routes * - * @var Route[] + * @var Route[][] */ private $routes = []; @@ -65,13 +65,6 @@ class Router */ private $publisher; - /** - * Name for the next route - * - * @var string|null - */ - private $name = null; - /** * Middleware (one or more) for next routes * @@ -127,14 +120,11 @@ public function __construct(string $namespacePrefix = '') public function group(array $attributes, Closure $routes): self { // Backup current properties - $oldName = $this->name; $oldMiddleware = $this->middleware; $oldNamespace = $this->namespace; $oldPrefix = $this->prefix; $oldDomain = $this->domain; - $this->name = null; - // Set middleware for the group if (isset($attributes[GroupAttributes::MIDDLEWARE])) { if (is_array($attributes[GroupAttributes::MIDDLEWARE]) == false) { @@ -163,7 +153,6 @@ public function group(array $attributes, Closure $routes): self call_user_func($routes, $this); // Restore properties - $this->name = $oldName; $this->domain = $oldDomain; $this->prefix = $oldPrefix; $this->middleware = $oldMiddleware; @@ -178,23 +167,17 @@ public function group(array $attributes, Closure $routes): self * @param string $method * @param string $route * @param Closure|callable|string $controller - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware - * @param string|null $domain * @param string|null $name * @return self */ public function map( - ?string $method, + string $method, string $route, $controller, - $middleware = [], - ?string $domain = null, ?string $name = null ): self { - $name = $name ?: $this->name; $uri = $this->prefix . $route; - $middleware = is_array($middleware) ? $middleware : [$middleware]; if (is_string($controller) && is_callable($controller) == false) { $controller = $this->namespace . "\\" . $controller; @@ -205,15 +188,14 @@ public function map( $uri, $method, $controller, - array_merge($this->middleware, $middleware), - $domain ?: $this->domain + $this->middleware, + $this->domain ); - $this->routes[] = $route; + $this->routes[$method][] = $route; if ($name) { $this->names[$name] = $route; - $this->name = null; } return $this; @@ -236,13 +218,16 @@ public function dispatch(): self $domain = $this->request->getUri()->getHost(); $uri = $this->request->getUri()->getPath(); - sort($this->routes, SORT_DESC); + $routes = array_merge( + $this->routes['*'] ?? [], + $this->routes[$method] ?? [] + ); + sort($routes, SORT_DESC); - foreach ($this->routes as $route) { + foreach ($routes as $route) { $parameters = []; if ( - $this->compareMethod($route->getMethod(), $method) && $this->compareDomain($route->getDomain(), $domain) && $this->compareUri($route->getUri(), $uri, $parameters) ) { @@ -257,18 +242,6 @@ public function dispatch(): self throw new RouteNotFoundException(); } - /** - * Check if given request method matches given route method - * - * @param string|null $routeMethod - * @param string $requestMethod - * @return bool - */ - private function compareMethod(?string $routeMethod, string $requestMethod): bool - { - return $routeMethod == null || $routeMethod == $requestMethod; - } - /** * Check if given request domain matches given route domain * @@ -518,20 +491,16 @@ private function regexParameter(string $name): string * * @param string $route * @param Closure|callable|string $controller - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware - * @param string|null $domain * @param string|null $name * @return self */ public function any( string $route, $controller, - $middleware = [], - ?string $domain = null, ?string $name = null ): self { - return $this->map(null, $route, $controller, $middleware, $domain, $name); + return $this->map('*', $route, $controller, $name); } /** @@ -539,20 +508,16 @@ public function any( * * @param string $route * @param Closure|callable|string $controller - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware - * @param string|null $domain * @param string|null $name * @return self */ public function get( string $route, $controller, - $middleware = [], - ?string $domain = null, ?string $name = null ): self { - return $this->map(HttpMethods::GET, $route, $controller, $middleware, $domain, $name); + return $this->map(HttpMethods::GET, $route, $controller, $name); } /** @@ -560,20 +525,16 @@ public function get( * * @param string $route * @param Closure|callable|string $controller - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware - * @param string|null $domain * @param string|null $name * @return self */ public function post( string $route, $controller, - $middleware = [], - ?string $domain = null, ?string $name = null ): self { - return $this->map(HttpMethods::POST, $route, $controller, $middleware, $domain, $name); + return $this->map(HttpMethods::POST, $route, $controller, $name); } /** @@ -581,20 +542,16 @@ public function post( * * @param string $route * @param Closure|callable|string $controller - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware - * @param string|null $domain * @param string|null $name * @return self */ public function put( string $route, $controller, - $middleware = [], - ?string $domain = null, ?string $name = null ): self { - return $this->map(HttpMethods::PUT, $route, $controller, $middleware, $domain, $name); + return $this->map(HttpMethods::PUT, $route, $controller, $name); } /** @@ -602,20 +559,16 @@ public function put( * * @param string $route * @param Closure|callable|string $controller - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware - * @param string|null $domain * @param string|null $name * @return self */ public function patch( string $route, $controller, - $middleware = [], - ?string $domain = null, ?string $name = null ): self { - return $this->map(HttpMethods::PATCH, $route, $controller, $middleware, $domain, $name); + return $this->map(HttpMethods::PATCH, $route, $controller, $name); } /** @@ -623,33 +576,16 @@ public function patch( * * @param string $route * @param Closure|callable|string $controller - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware - * @param string|null $domain * @param string|null $name * @return self */ public function delete( string $route, $controller, - $middleware = [], - ?string $domain = null, ?string $name = null ): self { - return $this->map(HttpMethods::DELETE, $route, $controller, $middleware, $domain, $name); - } - - /** - * Use given name for the next route mapping - * - * @param string $name - * @return self - */ - public function name(string $name): self - { - $this->name = $name; - - return $this; + return $this->map(HttpMethods::DELETE, $route, $controller, $name); } /** diff --git a/src/Values/Route.php b/src/Values/Route.php index 2d95a8e..efa3d4f 100644 --- a/src/Values/Route.php +++ b/src/Values/Route.php @@ -23,7 +23,7 @@ class Route private $uri; /** - * @var string|null + * @var string */ private $method; @@ -55,7 +55,7 @@ class Route public function __construct( ?string $name, string $uri, - ?string $method, + string $method, $controller, $middleware, ?string $domain @@ -118,7 +118,7 @@ public function getUri(): string /** * @return string|null */ - public function getMethod(): ?string + public function getMethod(): string { return $this->method; } diff --git a/tests/GroupingTest.php b/tests/GroupingTest.php index d80e2a8..08ddc88 100644 --- a/tests/GroupingTest.php +++ b/tests/GroupingTest.php @@ -38,24 +38,6 @@ public function test_with_a_middleware() $this->assertContains($middleware->content, SampleMiddleware::$output); } - /** - * @throws Throwable - */ - public function test_with_route_and_group_middleware() - { - $groupMiddleware = new SampleMiddleware(13); - $routeMiddleware = new SampleMiddleware(666); - - $router = $this->router() - ->group(['middleware' => $groupMiddleware], function (Router $router) use ($routeMiddleware) { - $router->get('/', $this->OkController(), $routeMiddleware); - })->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - $this->assertContains($groupMiddleware->content, SampleMiddleware::$output); - $this->assertContains($routeMiddleware->content, SampleMiddleware::$output); - } - /** * @throws Throwable */ @@ -138,21 +120,6 @@ public function test_with_domain() $this->assertEquals('OK', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_with_group_and_route_domain_it_should_only_consider_route_domain() - { - $this->mockRequest(HttpMethods::GET, 'http://sub2.domain.com/'); - - $router = $this->router() - ->group(['domain' => 'sub1.domain.com'], function (Router $router) { - $router->get('/', $this->OkController(), [], 'sub2.domain.com'); - })->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - /** * @throws Throwable */ @@ -170,19 +137,4 @@ public function test_nested_groups_with_domain_it_should_consider_the_inner_grou $this->assertEquals('OK', $this->output($router)); } - - /** - * @throws Throwable - */ - public function test_naming_it_should_remove_existing_name_before_the_group() - { - $router = $this->router() - ->name('NameForNothing') - ->group([], function (Router $router) { - $router->get('/', $this->OkController()); - })->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - $this->assertFalse($router->currentRoute()->getName() == 'NameForNothing'); - } } diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 7cb4490..b8019ae 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -4,6 +4,7 @@ use Closure; use MiladRahimi\PhpRouter\Exceptions\InvalidMiddlewareException; +use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Tests\Testing\SampleMiddleware; use MiladRahimi\PhpRouter\Tests\Testing\StopperMiddleware; use Throwable; @@ -18,9 +19,9 @@ public function test_with_a_single_middleware_as_an_object() { $middleware = new SampleMiddleware(666); - $router = $this->router() - ->get('/', $this->OkController(), $middleware) - ->dispatch(); + $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $r->get('/', $this->OkController()); + })->dispatch(); $this->assertEquals('OK', $this->output($router)); $this->assertContains($middleware->content, SampleMiddleware::$output); @@ -33,9 +34,9 @@ public function test_with_a_single_middleware_as_a_string() { $middleware = SampleMiddleware::class; - $router = $this->router() - ->get('/', $this->OkController(), $middleware) - ->dispatch(); + $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $r->get('/', $this->OkController()); + })->dispatch(); $this->assertEquals('OK', $this->output($router)); $this->assertEquals('empty', SampleMiddleware::$output[0]); @@ -50,11 +51,11 @@ public function test_with_a_single_middleware_as_a_closure() return $next($request->withAttribute('Middleware', 666)); }; - $router = $this->router() - ->get('/', function (ServerRequest $request) { + $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $r->get('/', function (ServerRequest $request) { return $request->getAttribute('Middleware'); - }, $middleware) - ->dispatch(); + }); + })->dispatch(); $this->assertEquals('666', $this->output($router)); } @@ -66,9 +67,9 @@ public function test_with_a_stopper_middleware() { $middleware = new StopperMiddleware(666); - $router = $this->router() - ->get('/', $this->OkController(), $middleware) - ->dispatch(); + $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $r->get('/', $this->OkController()); + })->dispatch(); $this->assertEquals('Stopped in middleware.', $this->output($router)); $this->assertContains($middleware->content, StopperMiddleware::$output); @@ -90,11 +91,11 @@ function (ServerRequest $request, $next) { }, ]; - $router = $this->router() - ->get('/', function (ServerRequest $request) { + $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $r->get('/', function (ServerRequest $request) { return $request->getAttribute('a') . ' ' . $request->getAttribute('b'); - }, $middleware) - ->dispatch(); + }); + })->dispatch(); $this->assertEquals('It works!', $this->output($router)); } @@ -106,8 +107,8 @@ public function test_with_invalid_middleware() { $this->expectException(InvalidMiddlewareException::class); - $this->router() - ->get('/', $this->OkController(), 'UnknownMiddleware') - ->dispatch(); + $this->router()->group(['middleware' => 'UnknownMiddleware'], function (Router $r) { + $r->get('/', $this->OkController()); + })->dispatch(); } } diff --git a/tests/NamingTest.php b/tests/NamingTest.php index 262dd1f..b73ade0 100644 --- a/tests/NamingTest.php +++ b/tests/NamingTest.php @@ -13,46 +13,21 @@ class NamingTest extends TestCase public function test_a_named_route() { $router = $this->router() - ->get('/', $this->OkController(), [], null, 'Home') + ->get('/', $this->OkController(), 'Home') ->dispatch(); $this->assertEquals('OK', $this->output($router)); $this->assertTrue($router->currentRoute()->getName() == 'Home'); } - /** - * @throws Throwable - */ - public function test_the_name_method() - { - $router = $this->router() - ->name('Home') - ->get('/', $this->OkController()) - ->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - $this->assertTrue($router->currentRoute()->getName() == 'Home'); - - $this->mockRequest(HttpMethods::POST, 'http://example.com/666'); - - $router = $this->router() - ->post('/{id}', function ($id) { - return $id; - }) - ->dispatch(); - - $this->assertEquals('666', $this->output($router)); - $this->assertFalse($router->currentRoute()->getName() == 'Home'); - } - /** * @throws Throwable */ public function test_duplicate_naming_it_should_set_the_name_for_all_routes() { $router = $this->router() - ->get('/', $this->OkController(), [], null, 'Home') - ->get('/home', $this->OkController(), [], null, 'Home') + ->get('/', $this->OkController(), 'Home') + ->get('/home', $this->OkController(), 'Home') ->dispatch(); $this->assertTrue($router->currentRoute()->getName() == 'Home'); diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index f454b4d..c7b71d3 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -216,34 +216,6 @@ public function test_with_a_optional_parameter_when_it_is_not_present() $this->assertEquals('Default', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_with_a_static_domain() - { - $this->mockRequest(HttpMethods::GET, 'http://server.domain.ext/'); - - $router = $this->router() - ->get('/', $this->OkController(), [], 'server.domain.ext') - ->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_with_a_domain_pattern() - { - $this->mockRequest(HttpMethods::GET, 'http://something.domain.ext/'); - - $router = $this->router() - ->get('/', $this->OkController(), [], '(.*).domain.ext') - ->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - /** * @throws Throwable */ @@ -317,11 +289,10 @@ public function test_injection_of_request_by_type() public function test_injection_of_router_by_name() { $router = $this->router() - ->name('home') ->get('/', function ($router) { /** @var Router $router */ return $router->currentRoute()->getName(); - }) + }, 'home') ->dispatch(); $this->assertEquals('home', $this->output($router)); @@ -333,10 +304,9 @@ public function test_injection_of_router_by_name() public function test_injection_of_router_by_type() { $router = $this->router() - ->name('home') ->get('/', function (Router $r) { return $r->currentRoute()->getName(); - }) + }, 'home') ->dispatch(); $this->assertEquals('home', $this->output($router)); @@ -459,7 +429,7 @@ public function test_with_invalid_controller_method() $this->expectException(InvalidControllerException::class); $namespace = 'MiladRahimi\PhpRouter\Tests\Testing'; - $this->router('', $namespace) + $this->router($namespace) ->get('/', 'SampleController@invalidMethod') ->dispatch(); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 72a514f..89b75f0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -39,13 +39,12 @@ protected function mockRequest(string $method, string $url): void /** * Get a router instance for testing purposes * - * @param string $prefix * @param string $namespace * @return Router */ - protected function router(string $prefix = '', string $namespace = ''): Router + protected function router(string $namespace = ''): Router { - $router = new Router($prefix, $namespace); + $router = new Router($namespace); $router->setPublisher(new TestPublisher()); return $router; diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 31ec267..8a16e2b 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -15,10 +15,9 @@ class UrlTest extends TestCase public function test_generating_url_for_the_homepage() { $router = $this->router() - ->name('home') ->get('/', function (Router $r) { return $r->url('home'); - }) + }, 'home') ->dispatch(); $this->assertEquals('/', $this->output($router)); @@ -32,12 +31,12 @@ public function test_generating_url_for_a_page() $this->mockRequest(HttpMethods::GET, 'http://web.com/page'); $router = $this->router() - ->name('home')->get('/', function (Router $r) { + ->get('/', function (Router $r) { return $r->url('home'); - }) - ->name('page')->get('/page', function (Router $r) { + }, 'home') + ->get('/page', function (Router $r) { return $r->url('page'); - }) + }, 'page') ->dispatch(); $this->assertEquals('/page', $this->output($router)); @@ -51,10 +50,9 @@ public function test_generating_url_for_a_page_with_required_parameter() $this->mockRequest(HttpMethods::GET, 'http://web.com/contact'); $router = $this->router() - ->name('page') ->get('/{name}', function (Router $r) { return $r->url('page', ['name' => 'about']); - }) + }, 'page') ->dispatch(); $this->assertEquals('/about', $this->output($router)); @@ -68,10 +66,9 @@ public function test_generating_url_for_a_page_with_optional_parameter() $this->mockRequest(HttpMethods::GET, 'http://web.com/contact'); $router = $this->router() - ->name('page') ->get('/{name?}', function (Router $r) { return $r->url('page', ['name' => 'about']); - }) + }, 'page') ->dispatch(); $this->assertEquals('/about', $this->output($router)); @@ -85,10 +82,9 @@ public function test_generating_url_for_a_page_with_optional_parameter_2() $this->mockRequest(HttpMethods::GET, 'http://web.com/contact'); $router = $this->router() - ->name('page') ->get('/{name?}', function (Router $r) { return $r->url('page'); - }) + }, 'page') ->dispatch(); $this->assertEquals('/', $this->output($router)); @@ -102,10 +98,9 @@ public function test_generating_url_for_a_page_with_optional_parameter_3() $this->mockRequest(HttpMethods::GET, 'http://web.com/page/contact'); $router = $this->router() - ->name('page') ->get('/page/?{name?}', function (Router $r) { return $r->url('page'); - }) + }, 'page') ->dispatch(); $this->assertEquals('/page', $this->output($router)); From 600a41b16bb9f73465c023c7620db5388e2c45a8 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 30 Mar 2020 14:37:36 +0430 Subject: [PATCH 08/52] add test for route getters --- tests/RoutingTest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index c7b71d3..7e6b608 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -2,6 +2,7 @@ namespace MiladRahimi\PhpRouter\Tests; +use Laminas\Diactoros\Response\JsonResponse; use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Exceptions\InvalidControllerException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; @@ -433,4 +434,25 @@ public function test_with_invalid_controller_method() ->get('/', 'SampleController@invalidMethod') ->dispatch(); } + + /** + * @throws Throwable + */ + public function test_current_route() + { + $router = $this->router() + ->get('/', function (Router $r) { + return join(',', [ + $r->currentRoute()->getName(), + $r->currentRoute()->getUri(), + $r->currentRoute()->getMethod(), + count($r->currentRoute()->getMiddleware()), + $r->currentRoute()->getDomain() ?? '-', + ]); + }, 'home') + ->dispatch(); + + $value = join(',', ['home', '/', 'GET', 0, '-']); + $this->assertEquals($value, $this->output($router)); + } } From af1ac417dc8f8383547b26d625e06e4193439bef Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 30 Mar 2020 15:20:46 +0430 Subject: [PATCH 09/52] some cleaning --- src/Router.php | 96 ++++++++++++++------------------------- src/Values/GroupState.php | 42 +++++++++++++++++ 2 files changed, 77 insertions(+), 61 deletions(-) create mode 100644 src/Values/GroupState.php diff --git a/src/Router.php b/src/Router.php index cc84e63..c949467 100644 --- a/src/Router.php +++ b/src/Router.php @@ -12,6 +12,7 @@ use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Values\Route; +use MiladRahimi\PhpRouter\Values\GroupState; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use ReflectionException; @@ -35,7 +36,7 @@ class Router /** * List of defined routes * - * @var Route[][] + * @var Route[][]|array[string][int]Route */ private $routes = []; @@ -54,6 +55,8 @@ class Router private $parameters = []; /** + * User HTTP Request + * * @var ServerRequestInterface */ private $request; @@ -66,32 +69,11 @@ class Router private $publisher; /** - * Middleware (one or more) for next routes - * - * @var string[]|callable[]|Closure[]|Middleware[] - */ - private $middleware = []; - - /** - * URI prefix for next routes - * - * @var string - */ - private $prefix; - - /** - * Controller namespace (prefix) for next routes + * The state holder for processing group * - * @var string + * @var GroupState */ - private $namespace; - - /** - * Domain for next routes - * - * @var string|null - */ - private $domain = null; + private $groupState; /** * Current route that is recognized for current request @@ -107,7 +89,8 @@ class Router */ public function __construct(string $namespacePrefix = '') { - $this->namespace = $namespacePrefix; + $this->groupState = new GroupState(); + $this->groupState->namespace = $namespacePrefix; } /** @@ -119,11 +102,8 @@ public function __construct(string $namespacePrefix = '') */ public function group(array $attributes, Closure $routes): self { - // Backup current properties - $oldMiddleware = $this->middleware; - $oldNamespace = $this->namespace; - $oldPrefix = $this->prefix; - $oldDomain = $this->domain; + // Backup group state + $gs = clone $this->groupState; // Set middleware for the group if (isset($attributes[GroupAttributes::MIDDLEWARE])) { @@ -131,32 +111,29 @@ public function group(array $attributes, Closure $routes): self $attributes[GroupAttributes::MIDDLEWARE] = [$attributes[GroupAttributes::MIDDLEWARE]]; } - $this->middleware = array_merge($attributes[GroupAttributes::MIDDLEWARE], $this->middleware); + $this->groupState->middleware = array_merge($attributes[GroupAttributes::MIDDLEWARE], $gs->middleware); } // Set namespace for the group if (isset($attributes[GroupAttributes::NAMESPACE])) { - $this->namespace = $attributes[GroupAttributes::NAMESPACE]; + $this->groupState->namespace = $gs->namespace . "\\" . $attributes[GroupAttributes::NAMESPACE]; } // Set prefix for the group if (isset($attributes[GroupAttributes::PREFIX])) { - $this->prefix = $this->prefix . $attributes[GroupAttributes::PREFIX]; + $this->groupState->prefix = $gs->prefix . $attributes[GroupAttributes::PREFIX]; } // Set domain for the group if (isset($attributes[GroupAttributes::DOMAIN])) { - $this->domain = $attributes[GroupAttributes::DOMAIN]; + $this->groupState->domain = $attributes[GroupAttributes::DOMAIN]; } // Run the group body closure call_user_func($routes, $this); - // Restore properties - $this->domain = $oldDomain; - $this->prefix = $oldPrefix; - $this->middleware = $oldMiddleware; - $this->namespace = $oldNamespace; + // Restore group state + $this->groupState = $gs; return $this; } @@ -177,10 +154,10 @@ public function map( ?string $name = null ): self { - $uri = $this->prefix . $route; + $uri = $this->groupState->prefix . $route; if (is_string($controller) && is_callable($controller) == false) { - $controller = $this->namespace . "\\" . $controller; + $controller = $this->groupState->namespace . "\\" . $controller; } $route = new Route( @@ -188,15 +165,13 @@ public function map( $uri, $method, $controller, - $this->middleware, - $this->domain + $this->groupState->middleware, + $this->groupState->domain ); $this->routes[$method][] = $route; - if ($name) { - $this->names[$name] = $route; - } + $name && $this->names[$name] = $route; return $this; } @@ -222,13 +197,14 @@ public function dispatch(): self $this->routes['*'] ?? [], $this->routes[$method] ?? [] ); + sort($routes, SORT_DESC); foreach ($routes as $route) { $parameters = []; if ( - $this->compareDomain($route->getDomain(), $domain) && + (!$route->getDomain() || $this->compareDomain($route->getDomain(), $domain)) && $this->compareUri($route->getUri(), $uri, $parameters) ) { $this->currentRoute = $route; @@ -251,7 +227,7 @@ public function dispatch(): self */ private function compareDomain(?string $routeDomain, string $requestDomain): bool { - return $routeDomain == null || preg_match('@^' . $routeDomain . '$@', $requestDomain); + return preg_match('@^' . $routeDomain . '$@', $requestDomain); } /** @@ -264,9 +240,7 @@ private function compareDomain(?string $routeDomain, string $requestDomain): boo */ private function compareUri(string $routeUri, string $requestUri, array &$parameters): bool { - $pattern = '@^' . $this->regexUri($routeUri) . '$@'; - - return preg_match($pattern, $requestUri, $parameters); + return preg_match('@^' . $this->regexUri($routeUri) . '$@', $requestUri, $parameters); } /** @@ -427,17 +401,17 @@ function (ReflectionParameter $parameter) use ($parameters, $request) { /** @noinspection PhpPossiblePolymorphicInvocationInspection */ if ( + ($parameter->getName() == 'request') || ($parameter->getType() && $parameter->getType()->getName() == ServerRequestInterface::class) || - ($parameter->getType() && $parameter->getType()->getName() == ServerRequest::class) || - ($parameter->getName() == 'request') + ($parameter->getType() && $parameter->getType()->getName() == ServerRequest::class) ) { return $request; } /** @noinspection PhpPossiblePolymorphicInvocationInspection */ if ( - ($parameter->getType() && $parameter->getType()->getName() == Router::class) || - ($parameter->getName() == 'router') + ($parameter->getName() == 'router') || + ($parameter->getType() && $parameter->getType()->getName() == Router::class) ) { return $this; } @@ -605,18 +579,18 @@ public function define(string $name, string $pattern): self /** * Generate URL for given route name * - * @param string $routeName + * @param string $route * @param string[] $parameters * @return string * @throws UndefinedRouteException */ - public function url(string $routeName, array $parameters = []): string + public function url(string $route, array $parameters = []): string { - if (isset($this->names[$routeName]) == false) { - throw new UndefinedRouteException("There is no route with name `$routeName`."); + if (isset($this->names[$route]) == false) { + throw new UndefinedRouteException("There is no route with name `$route`."); } - $uri = $this->names[$routeName]->getUri(); + $uri = $this->names[$route]->getUri(); foreach ($parameters as $name => $value) { $uri = preg_replace('/\??{' . $name . '\??}/', $value, $uri); diff --git a/src/Values/GroupState.php b/src/Values/GroupState.php new file mode 100644 index 0000000..729ff6c --- /dev/null +++ b/src/Values/GroupState.php @@ -0,0 +1,42 @@ + Date: Mon, 30 Mar 2020 16:20:48 +0430 Subject: [PATCH 10/52] rename testpublisher to fakepublisher --- src/Router.php | 16 ++++++++-------- tests/TestCase.php | 8 ++++---- .../{TestPublisher.php => FakePublisher.php} | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) rename tests/Testing/{TestPublisher.php => FakePublisher.php} (96%) diff --git a/src/Router.php b/src/Router.php index c949467..a880654 100644 --- a/src/Router.php +++ b/src/Router.php @@ -57,16 +57,16 @@ class Router /** * User HTTP Request * - * @var ServerRequestInterface + * @var ServerRequestInterface|null */ - private $request; + private $request = null; /** * The publisher that is going to publish outputs of controllers * - * @var Publisher + * @var Publisher|null */ - private $publisher; + private $publisher = null; /** * The state holder for processing group @@ -622,9 +622,9 @@ private function prepare(): void /** * Get current http request instance * - * @return ServerRequestInterface + * @return ServerRequestInterface|null */ - public function getRequest(): ServerRequestInterface + public function getRequest(): ?ServerRequestInterface { return $this->request; } @@ -640,9 +640,9 @@ public function setRequest(ServerRequestInterface $request): void } /** - * @return Publisher + * @return Publisher|null */ - public function getPublisher(): Publisher + public function getPublisher(): ?Publisher { return $this->publisher; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 89b75f0..de816bc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,7 +6,7 @@ use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; -use MiladRahimi\PhpRouter\Tests\Testing\TestPublisher; +use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -45,7 +45,7 @@ protected function mockRequest(string $method, string $url): void protected function router(string $namespace = ''): Router { $router = new Router($namespace); - $router->setPublisher(new TestPublisher()); + $router->setPublisher(new FakePublisher()); return $router; } @@ -77,9 +77,9 @@ protected function output(Router $router) * Get the given router publisher. * * @param Router $router - * @return TestPublisher|Publisher + * @return FakePublisher|Publisher */ - protected function publisher(Router $router): TestPublisher + protected function publisher(Router $router): FakePublisher { return $router->getPublisher(); } diff --git a/tests/Testing/TestPublisher.php b/tests/Testing/FakePublisher.php similarity index 96% rename from tests/Testing/TestPublisher.php rename to tests/Testing/FakePublisher.php index b524108..9027f97 100644 --- a/tests/Testing/TestPublisher.php +++ b/tests/Testing/FakePublisher.php @@ -5,7 +5,7 @@ use MiladRahimi\PhpRouter\Services\Publisher; use Psr\Http\Message\ResponseInterface; -class TestPublisher implements Publisher +class FakePublisher implements Publisher { /** * @var string From ba32110e61f8d9288ef446981924741466c9653a Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 30 Mar 2020 17:36:28 +0430 Subject: [PATCH 11/52] add more tests --- tests/Enums/HttpMethodsTest.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/Enums/HttpMethodsTest.php b/tests/Enums/HttpMethodsTest.php index 63e2e27..d4ed6d5 100644 --- a/tests/Enums/HttpMethodsTest.php +++ b/tests/Enums/HttpMethodsTest.php @@ -7,10 +7,24 @@ class HttpMethodsTest extends TestCase { - public function test_custom_method_it_should_standardize_the_method() + public function test_custom_method_with_lowercase_method_it_should_standardize_it() { $actual = HttpMethods::custom('append'); $this->assertEquals('APPEND', $actual); } + + public function test_custom_method_with_uppercase_method_it_should_standardize_it() + { + $actual = HttpMethods::custom('APPEND'); + + $this->assertEquals('APPEND', $actual); + } + + public function test_custom_method_with_mixed_case_method_it_should_standardize_it() + { + $actual = HttpMethods::custom('AppEnd'); + + $this->assertEquals('APPEND', $actual); + } } From 2f317409a9f1184abf64298a5bef234b9b24a41d Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Fri, 1 May 2020 01:16:47 +0430 Subject: [PATCH 12/52] update state --- src/Router.php | 48 ++++++++++++------------ src/Values/{GroupState.php => State.php} | 4 +- tests/RoutingTest.php | 28 -------------- tests/TestCase.php | 7 ++-- 4 files changed, 30 insertions(+), 57 deletions(-) rename src/Values/{GroupState.php => State.php} (93%) diff --git a/src/Router.php b/src/Router.php index a880654..f337f97 100644 --- a/src/Router.php +++ b/src/Router.php @@ -12,7 +12,7 @@ use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Values\Route; -use MiladRahimi\PhpRouter\Values\GroupState; +use MiladRahimi\PhpRouter\Values\State; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use ReflectionException; @@ -69,11 +69,12 @@ class Router private $publisher = null; /** - * The state holder for processing group + * The state of current instance/group + * It holds current attributes like prefix, domain... * - * @var GroupState + * @var State */ - private $groupState; + private $state; /** * Current route that is recognized for current request @@ -85,55 +86,54 @@ class Router /** * Router constructor. * - * @param string $namespacePrefix + * @param State|null $state */ - public function __construct(string $namespacePrefix = '') + public function __construct(?State $state = null) { - $this->groupState = new GroupState(); - $this->groupState->namespace = $namespacePrefix; + $this->state = $state ?: new State(); } /** * Group routes with the given attributes * * @param array $attributes - * @param Closure $routes + * @param Closure $body * @return self */ - public function group(array $attributes, Closure $routes): self + public function group(array $attributes, Closure $body): self { // Backup group state - $gs = clone $this->groupState; + $state = clone $this->state; // Set middleware for the group if (isset($attributes[GroupAttributes::MIDDLEWARE])) { if (is_array($attributes[GroupAttributes::MIDDLEWARE]) == false) { - $attributes[GroupAttributes::MIDDLEWARE] = [$attributes[GroupAttributes::MIDDLEWARE]]; + $this->state->middleware[] = $attributes[GroupAttributes::MIDDLEWARE]; + } else { + $this->state->middleware = array_merge($state->middleware, $attributes[GroupAttributes::MIDDLEWARE]); } - - $this->groupState->middleware = array_merge($attributes[GroupAttributes::MIDDLEWARE], $gs->middleware); } // Set namespace for the group if (isset($attributes[GroupAttributes::NAMESPACE])) { - $this->groupState->namespace = $gs->namespace . "\\" . $attributes[GroupAttributes::NAMESPACE]; + $this->state->namespace = join("\\", [$state->namespace, $attributes[GroupAttributes::NAMESPACE]]); } // Set prefix for the group if (isset($attributes[GroupAttributes::PREFIX])) { - $this->groupState->prefix = $gs->prefix . $attributes[GroupAttributes::PREFIX]; + $this->state->prefix = $state->prefix . $attributes[GroupAttributes::PREFIX]; } // Set domain for the group if (isset($attributes[GroupAttributes::DOMAIN])) { - $this->groupState->domain = $attributes[GroupAttributes::DOMAIN]; + $this->state->domain = $attributes[GroupAttributes::DOMAIN]; } // Run the group body closure - call_user_func($routes, $this); + call_user_func($body, $this); - // Restore group state - $this->groupState = $gs; + // Revert to the old state + $this->state = $state; return $this; } @@ -154,10 +154,10 @@ public function map( ?string $name = null ): self { - $uri = $this->groupState->prefix . $route; + $uri = $this->state->prefix . $route; if (is_string($controller) && is_callable($controller) == false) { - $controller = $this->groupState->namespace . "\\" . $controller; + $controller = join("\\", [$this->state->namespace, $controller]); } $route = new Route( @@ -165,8 +165,8 @@ public function map( $uri, $method, $controller, - $this->groupState->middleware, - $this->groupState->domain + $this->state->middleware, + $this->state->domain ); $this->routes[$method][] = $route; diff --git a/src/Values/GroupState.php b/src/Values/State.php similarity index 93% rename from src/Values/GroupState.php rename to src/Values/State.php index 729ff6c..c11e9a0 100644 --- a/src/Values/GroupState.php +++ b/src/Values/State.php @@ -6,11 +6,11 @@ use MiladRahimi\PhpRouter\Middleware; /** - * Class GroupState + * Class State * * @package MiladRahimi\PhpRouter\Values */ -class GroupState +class State { /** * URI prefix diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 7e6b608..6550d2c 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -2,7 +2,6 @@ namespace MiladRahimi\PhpRouter\Tests; -use Laminas\Diactoros\Response\JsonResponse; use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Exceptions\InvalidControllerException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; @@ -376,20 +375,6 @@ public function test_with_fully_namespaced_controller() $this->assertEquals('Home', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_with_preserved_namespaced_controller() - { - $namespace = 'MiladRahimi\PhpRouter\Tests\Testing'; - - $router = $this->router($namespace) - ->get('/', 'SampleController@home') - ->dispatch(); - - $this->assertEquals('Home', $this->output($router)); - } - /** * @throws Throwable */ @@ -422,19 +407,6 @@ public function test_with_invalid_controller_class() $this->router()->get('/', 666)->dispatch(); } - /** - * @throws Throwable - */ - public function test_with_invalid_controller_method() - { - $this->expectException(InvalidControllerException::class); - - $namespace = 'MiladRahimi\PhpRouter\Tests\Testing'; - $this->router($namespace) - ->get('/', 'SampleController@invalidMethod') - ->dispatch(); - } - /** * @throws Throwable */ diff --git a/tests/TestCase.php b/tests/TestCase.php index de816bc..0481773 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,6 +7,7 @@ use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; +use MiladRahimi\PhpRouter\Values\State; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -39,12 +40,12 @@ protected function mockRequest(string $method, string $url): void /** * Get a router instance for testing purposes * - * @param string $namespace + * @param State|null $state * @return Router */ - protected function router(string $namespace = ''): Router + protected function router(State $state = null): Router { - $router = new Router($namespace); + $router = new Router($state); $router->setPublisher(new FakePublisher()); return $router; From 63ae90fcf2afbbac5a00080c060f36927808df78 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Fri, 1 May 2020 01:26:28 +0430 Subject: [PATCH 13/52] update state --- tests/RoutingTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 6550d2c..4acb5a9 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -6,6 +6,7 @@ use MiladRahimi\PhpRouter\Exceptions\InvalidControllerException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Router; +use MiladRahimi\PhpRouter\Tests\Testing\SampleController; use Psr\Http\Message\ServerRequestInterface; use Throwable; use Laminas\Diactoros\ServerRequest; @@ -397,6 +398,16 @@ public function test_with_class_method_but_invalid_controller_class() $this->router()->get('/', 'UnknownController@method')->dispatch(); } + /** + * @throws Throwable + */ + public function test_with_class_but_invalid_method() + { + $this->expectException(InvalidControllerException::class); + + $this->router()->get('/', SampleController::class . '@invalid')->dispatch(); + } + /** * @throws Throwable */ From f6f83c956b689c65ef45971dca4dd0851c80f865 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 17 Aug 2020 04:26:44 +0430 Subject: [PATCH 14/52] update --- README.md | 2 +- examples/example-19/index.php | 17 ++++++-- src/Router.php | 57 +++++++++++++------------ src/Services/Container.php | 14 +++++++ src/Services/SimpleContainer.php | 8 ++++ src/Values/{State.php => Config.php} | 4 +- src/Values/Route.php | 63 +++++++++++++++++++++++++--- tests/RoutingTest.php | 2 +- tests/TestCase.php | 8 ++-- 9 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 src/Services/Container.php create mode 100644 src/Services/SimpleContainer.php rename src/Values/{State.php => Config.php} (95%) diff --git a/README.md b/README.md index 1a1832b..f5e6924 100644 --- a/README.md +++ b/README.md @@ -532,7 +532,7 @@ $router = new Router(); $router->name('home')->get('/', function (Router $router) { return new JsonResponse([ 'current_page_name' => $router->currentRoute()->getName(), /* Result: home */ - 'current_page_uri' => $router->currentRoute()->getUri(), /* Result: / */ + 'current_page_uri' => $router->currentRoute()->getPath(), /* Result: / */ 'current_page_method' => $router->currentRoute()->getMethod(), /* Result: GET */ 'current_page_domain' => $router->currentRoute()->getDomain(), /* Result: null */ ]); diff --git a/examples/example-19/index.php b/examples/example-19/index.php index e30ffb2..e3ca619 100644 --- a/examples/example-19/index.php +++ b/examples/example-19/index.php @@ -7,13 +7,24 @@ $router = new Router(); -$router->name('home')->get('/', function (Router $router) { +$router->get('/', function (Router $router) { return new JsonResponse([ 'current_page_name' => $router->currentRoute()->getName(), /* Result: home */ - 'current_page_uri' => $router->currentRoute()->getUri(), /* Result: / */ + 'current_page_path' => $router->currentRoute()->getPath(), /* Result: / */ 'current_page_method' => $router->currentRoute()->getMethod(), /* Result: GET */ 'current_page_domain' => $router->currentRoute()->getDomain(), /* Result: null */ ]); -}); +}, 'home'); + +$router->get('/{var1}/{var2}', function (Router $router) { + return new JsonResponse([ + 'current_page_name' => $router->currentRoute()->getName(), /* Result: home */ + 'current_page_path' => $router->currentRoute()->getPath(), /* Result: / */ + 'current_page_method' => $router->currentRoute()->getMethod(), /* Result: GET */ + 'current_page_domain' => $router->currentRoute()->getDomain(), /* Result: null */ + 'current_page_params' => $router->currentRoute()->getParameters(), /* Result: null */ + 'current_page_uri' => $router->currentRoute()->getUri(), /* Result: null */ + ]); +}, 'page'); $router->dispatch(); diff --git a/src/Router.php b/src/Router.php index f337f97..d67c642 100644 --- a/src/Router.php +++ b/src/Router.php @@ -12,7 +12,7 @@ use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Values\Route; -use MiladRahimi\PhpRouter\Values\State; +use MiladRahimi\PhpRouter\Values\Config; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use ReflectionException; @@ -69,12 +69,12 @@ class Router private $publisher = null; /** - * The state of current instance/group + * The configuration of current instance/group * It holds current attributes like prefix, domain... * - * @var State + * @var Config */ - private $state; + private $config; /** * Current route that is recognized for current request @@ -86,11 +86,11 @@ class Router /** * Router constructor. * - * @param State|null $state + * @param Config|null $config */ - public function __construct(?State $state = null) + public function __construct(?Config $config = null) { - $this->state = $state ?: new State(); + $this->config = $config ?: new Config(); } /** @@ -102,38 +102,38 @@ public function __construct(?State $state = null) */ public function group(array $attributes, Closure $body): self { - // Backup group state - $state = clone $this->state; + // Backup group config + $config = clone $this->config; // Set middleware for the group if (isset($attributes[GroupAttributes::MIDDLEWARE])) { if (is_array($attributes[GroupAttributes::MIDDLEWARE]) == false) { - $this->state->middleware[] = $attributes[GroupAttributes::MIDDLEWARE]; + $this->config->middleware[] = $attributes[GroupAttributes::MIDDLEWARE]; } else { - $this->state->middleware = array_merge($state->middleware, $attributes[GroupAttributes::MIDDLEWARE]); + $this->config->middleware = array_merge($config->middleware, $attributes[GroupAttributes::MIDDLEWARE]); } } // Set namespace for the group if (isset($attributes[GroupAttributes::NAMESPACE])) { - $this->state->namespace = join("\\", [$state->namespace, $attributes[GroupAttributes::NAMESPACE]]); + $this->config->namespace = join("\\", [$config->namespace, $attributes[GroupAttributes::NAMESPACE]]); } // Set prefix for the group if (isset($attributes[GroupAttributes::PREFIX])) { - $this->state->prefix = $state->prefix . $attributes[GroupAttributes::PREFIX]; + $this->config->prefix = $config->prefix . $attributes[GroupAttributes::PREFIX]; } // Set domain for the group if (isset($attributes[GroupAttributes::DOMAIN])) { - $this->state->domain = $attributes[GroupAttributes::DOMAIN]; + $this->config->domain = $attributes[GroupAttributes::DOMAIN]; } // Run the group body closure call_user_func($body, $this); - // Revert to the old state - $this->state = $state; + // Revert to the old config + $this->config = $config; return $this; } @@ -154,10 +154,10 @@ public function map( ?string $name = null ): self { - $uri = $this->state->prefix . $route; + $uri = $this->config->prefix . $route; if (is_string($controller) && is_callable($controller) == false) { - $controller = join("\\", [$this->state->namespace, $controller]); + $controller = join("\\", [$this->config->namespace, $controller]); } $route = new Route( @@ -165,8 +165,8 @@ public function map( $uri, $method, $controller, - $this->state->middleware, - $this->state->domain + $this->config->middleware, + $this->config->domain ); $this->routes[$method][] = $route; @@ -193,6 +193,9 @@ public function dispatch(): self $domain = $this->request->getUri()->getHost(); $uri = $this->request->getUri()->getPath(); + /** + * @var Route[] $routes + */ $routes = array_merge( $this->routes['*'] ?? [], $this->routes[$method] ?? [] @@ -205,8 +208,10 @@ public function dispatch(): self if ( (!$route->getDomain() || $this->compareDomain($route->getDomain(), $domain)) && - $this->compareUri($route->getUri(), $uri, $parameters) + $this->compareUri($route->getPath(), $uri, $parameters) ) { + $route->setParameters($parameters); + $route->setUri($uri); $this->currentRoute = $route; $this->publisher->publish($this->run($route, $parameters)); @@ -233,14 +238,14 @@ private function compareDomain(?string $routeDomain, string $requestDomain): boo /** * Check if given request uri matches given uri method * - * @param string $routeUri - * @param string $requestUri + * @param string $path + * @param string $uri * @param array $parameters * @return bool */ - private function compareUri(string $routeUri, string $requestUri, array &$parameters): bool + private function compareUri(string $path, string $uri, array &$parameters): bool { - return preg_match('@^' . $this->regexUri($routeUri) . '$@', $requestUri, $parameters); + return preg_match('@^' . $this->regexUri($path) . '$@', $uri, $parameters); } /** @@ -590,7 +595,7 @@ public function url(string $route, array $parameters = []): string throw new UndefinedRouteException("There is no route with name `$route`."); } - $uri = $this->names[$route]->getUri(); + $uri = $this->names[$route]->getPath(); foreach ($parameters as $name => $value) { $uri = preg_replace('/\??{' . $name . '\??}/', $value, $uri); diff --git a/src/Services/Container.php b/src/Services/Container.php new file mode 100644 index 0000000..0fdc47d --- /dev/null +++ b/src/Services/Container.php @@ -0,0 +1,14 @@ +name = $name; - $this->uri = $uri; + $this->path = $uri; $this->method = $method; $this->controller = $controller; $this->middleware = $middleware; @@ -75,7 +86,7 @@ public function toArray(): array { return [ 'name' => $this->name, - 'uri' => $this->uri, + 'uri' => $this->path, 'method' => $this->method, 'controller' => $this->controller, 'middleware' => $this->middleware, @@ -110,9 +121,9 @@ public function getName(): ?string /** * @return string */ - public function getUri(): string + public function getPath(): string { - return $this->uri; + return $this->path; } /** @@ -146,4 +157,44 @@ public function getDomain(): ?string { return $this->domain; } + + /** + * @return array|string[]|null + */ + public function getParameters(): ?array + { + return $this->parameters; + } + + /** + * @param array|string[]|null $parameters + */ + public function setParameters(?array $parameters): void + { + $routeParameters = []; + + foreach ($parameters as $key => $value) { + if (strpos($this->path, $key) !== false) { + $routeParameters[$key] = $value; + } + } + + $this->parameters = $routeParameters; + } + + /** + * @return string|null + */ + public function getUri(): ?string + { + return $this->uri; + } + + /** + * @param string|null $uri + */ + public function setUri(?string $uri): void + { + $this->uri = $uri; + } } diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 4acb5a9..da028ea 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -427,7 +427,7 @@ public function test_current_route() ->get('/', function (Router $r) { return join(',', [ $r->currentRoute()->getName(), - $r->currentRoute()->getUri(), + $r->currentRoute()->getPath(), $r->currentRoute()->getMethod(), count($r->currentRoute()->getMiddleware()), $r->currentRoute()->getDomain() ?? '-', diff --git a/tests/TestCase.php b/tests/TestCase.php index 0481773..4caf630 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,7 +7,7 @@ use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; -use MiladRahimi\PhpRouter\Values\State; +use MiladRahimi\PhpRouter\Values\Config; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -40,12 +40,12 @@ protected function mockRequest(string $method, string $url): void /** * Get a router instance for testing purposes * - * @param State|null $state + * @param Config|null $config * @return Router */ - protected function router(State $state = null): Router + protected function router(Config $config = null): Router { - $router = new Router($state); + $router = new Router($config); $router->setPublisher(new FakePublisher()); return $router; From de10d7f8f9078494e2505e8cb777f1d9e573f3dd Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 17 Aug 2020 05:14:54 +0430 Subject: [PATCH 15/52] update --- src/Services/Container.php | 14 -------------- src/Services/SimpleContainer.php | 8 -------- 2 files changed, 22 deletions(-) delete mode 100644 src/Services/Container.php delete mode 100644 src/Services/SimpleContainer.php diff --git a/src/Services/Container.php b/src/Services/Container.php deleted file mode 100644 index 0fdc47d..0000000 --- a/src/Services/Container.php +++ /dev/null @@ -1,14 +0,0 @@ - Date: Mon, 17 Aug 2020 05:19:30 +0430 Subject: [PATCH 16/52] update --- src/Values/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Values/Route.php b/src/Values/Route.php index a0c9d02..d61ca8a 100644 --- a/src/Values/Route.php +++ b/src/Values/Route.php @@ -174,7 +174,7 @@ public function setParameters(?array $parameters): void $routeParameters = []; foreach ($parameters as $key => $value) { - if (strpos($this->path, $key) !== false) { + if (strpos($this->path ?? '', $key) !== false) { $routeParameters[$key] = $value; } } From 26fb19f715b6ed60f74d1faea848f2b195084759 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 17 Aug 2020 05:24:12 +0430 Subject: [PATCH 17/52] update --- src/Values/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Values/Route.php b/src/Values/Route.php index d61ca8a..6a00996 100644 --- a/src/Values/Route.php +++ b/src/Values/Route.php @@ -174,7 +174,7 @@ public function setParameters(?array $parameters): void $routeParameters = []; foreach ($parameters as $key => $value) { - if (strpos($this->path ?? '', $key) !== false) { + if (strstr($this->path, $key) !== false) { $routeParameters[$key] = $value; } } From 0ee7186d45ef93a61677fa3ef44b82790673db58 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 17 Aug 2020 05:27:54 +0430 Subject: [PATCH 18/52] update --- src/Values/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Values/Route.php b/src/Values/Route.php index 6a00996..c1603ca 100644 --- a/src/Values/Route.php +++ b/src/Values/Route.php @@ -174,7 +174,7 @@ public function setParameters(?array $parameters): void $routeParameters = []; foreach ($parameters as $key => $value) { - if (strstr($this->path, $key) !== false) { + if (strstr((string)$this->path, (string)$key) !== false) { $routeParameters[$key] = $value; } } From 169f14b33774236e39d963e1d0e1722cb1d8b965 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Wed, 19 Aug 2020 05:16:20 +0430 Subject: [PATCH 19/52] update --- composer.json | 3 +- src/Router.php | 145 +++++++++++++++--------------------------- src/Values/Route.php | 10 +-- tests/RoutingTest.php | 4 +- 4 files changed, 58 insertions(+), 104 deletions(-) diff --git a/composer.json b/composer.json index 6e2fc07..47a614d 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "php": ">=7.1", "ext-json": "*", "ext-mbstring": "*", - "laminas/laminas-diactoros": "^2.2" + "laminas/laminas-diactoros": "^2.2", + "miladrahimi/phpcontainer": "^4.2" }, "require-dev": { "phpunit/phpunit": "^7" diff --git a/src/Router.php b/src/Router.php index d67c642..9a4d048 100644 --- a/src/Router.php +++ b/src/Router.php @@ -3,6 +3,9 @@ namespace MiladRahimi\PhpRouter; use Closure; +use MiladRahimi\PhpContainer\Container; +use MiladRahimi\PhpContainer\Exceptions\ContainerException; +use MiladRahimi\PhpContainer\Exceptions\NotFoundException; use MiladRahimi\PhpRouter\Enums\GroupAttributes; use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Exceptions\InvalidControllerException; @@ -13,13 +16,9 @@ use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Values\Route; use MiladRahimi\PhpRouter\Values\Config; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use ReflectionException; -use ReflectionFunction; -use ReflectionFunctionAbstract; -use ReflectionMethod; -use ReflectionParameter; use Throwable; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; @@ -83,6 +82,13 @@ class Router */ private $currentRoute = null; + /** + * The dependency injection container + * + * @var Container + */ + private $container; + /** * Router constructor. * @@ -91,6 +97,7 @@ class Router public function __construct(?Config $config = null) { $this->config = $config ?: new Config(); + $this->container = new Container(); } /** @@ -210,6 +217,12 @@ public function dispatch(): self (!$route->getDomain() || $this->compareDomain($route->getDomain(), $domain)) && $this->compareUri($route->getPath(), $uri, $parameters) ) { + foreach ($parameters as $key => $value) { + if (strstr($route->getPath(), (string)$key) === false) { + unset($parameters[$key]); + } + } + $route->setParameters($parameters); $route->setUri($uri); $this->currentRoute = $route; @@ -321,7 +334,8 @@ private function runControllerThroughMiddleware( * @param ServerRequestInterface $request * @return ResponseInterface|mixed|null * @throws InvalidControllerException - * @throws ReflectionException + * @throws ContainerException + * @throws NotFoundException */ private function runController($controller, array $parameters, ServerRequestInterface $request) { @@ -338,98 +352,22 @@ private function runController($controller, array $parameters, ServerRequestInte throw new InvalidControllerException("Controller method `$methodName` not found."); } - $parameters = $this->arrangeMethodParameters($className, $methodName, $parameters, $request); - $controller = [$classObject, $methodName]; - } elseif (is_callable($controller)) { - $parameters = $this->arrangeFunctionParameters($controller, $parameters, $request); - } else { - throw new InvalidControllerException('Invalid controller: ' . $controller); } - return call_user_func_array($controller, $parameters); - } - - /** - * Arrange parameters for given function - * - * @param Closure|callable $function - * @param array $parameters - * @param ServerRequestInterface $request - * @return array - * @throws ReflectionException - */ - private function arrangeFunctionParameters($function, array $parameters, ServerRequestInterface $request): array - { - return $this->arrangeParameters(new ReflectionFunction($function), $parameters, $request); - } - - /** - * Arrange parameters for given method - * - * @param string $class - * @param string $method - * @param array $parameters - * @param ServerRequestInterface $request - * @return array - * @throws ReflectionException - */ - private function arrangeMethodParameters( - string $class, - string $method, - array $parameters, - ServerRequestInterface $request - ): array - { - return $this->arrangeParameters(new ReflectionMethod($class, $method), $parameters, $request); - } - - /** - * Arrange parameters for given method/function - * - * @param ReflectionFunctionAbstract $reflection - * @param array $parameters - * @param ServerRequestInterface $request - * @return array - */ - private function arrangeParameters( - ReflectionFunctionAbstract $reflection, - array $parameters, - ServerRequestInterface $request - ): array - { - return array_map( - function (ReflectionParameter $parameter) use ($parameters, $request) { - if (isset($parameters[$parameter->getName()])) { - return $parameters[$parameter->getName()]; - } - - /** @noinspection PhpPossiblePolymorphicInvocationInspection */ - if ( - ($parameter->getName() == 'request') || - ($parameter->getType() && $parameter->getType()->getName() == ServerRequestInterface::class) || - ($parameter->getType() && $parameter->getType()->getName() == ServerRequest::class) - ) { - return $request; - } - - /** @noinspection PhpPossiblePolymorphicInvocationInspection */ - if ( - ($parameter->getName() == 'router') || - ($parameter->getType() && $parameter->getType()->getName() == Router::class) - ) { - return $this; - } + if (is_callable($controller) == false) { + throw new InvalidControllerException('Invalid controller: ' . $controller); + } - if ($parameter->isOptional()) { - return $parameter->getDefaultValue(); - } + $this->container->singleton('$request', $request); + $this->container->singleton(ServerRequest::class, $request); + $this->container->singleton(ServerRequestInterface::class, $request); - return null; - }, + foreach ($parameters as $key => $value) { + $this->container->singleton('$' . $key, $value); + } - $reflection->getParameters() - ); + return $this->container->call($controller); } /** @@ -622,6 +560,13 @@ private function prepare(): void { $this->request = $this->request ?: ServerRequestFactory::fromGlobals(); $this->publisher = $this->publisher ?: new HttpPublisher(); + + $this->container->singleton('$router', $this); + $this->container->singleton(Router::class, $this); + + $this->container->singleton('$container', $this->container); + $this->container->singleton(Container::class, $this->container); + $this->container->singleton(ContainerInterface::class, $this->container); } /** @@ -659,4 +604,20 @@ public function setPublisher(Publisher $publisher): void { $this->publisher = $publisher; } + + /** + * @return Container + */ + public function getContainer(): Container + { + return $this->container; + } + + /** + * @param Container $container + */ + public function setContainer(Container $container): void + { + $this->container = $container; + } } diff --git a/src/Values/Route.php b/src/Values/Route.php index c1603ca..e670a83 100644 --- a/src/Values/Route.php +++ b/src/Values/Route.php @@ -171,15 +171,7 @@ public function getParameters(): ?array */ public function setParameters(?array $parameters): void { - $routeParameters = []; - - foreach ($parameters as $key => $value) { - if (strstr((string)$this->path, (string)$key) !== false) { - $routeParameters[$key] = $value; - } - } - - $this->parameters = $routeParameters; + $this->parameters = $parameters; } /** diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index da028ea..077e3a6 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -209,8 +209,8 @@ public function test_with_a_optional_parameter_when_it_is_not_present() $this->mockRequest(HttpMethods::GET, 'http://web.com/'); $router = $this->router() - ->get('/{id?}', function ($id) { - return $id ?: 'Default'; + ->get('/{id?}', function ($id = 'Default') { + return $id; }) ->dispatch(); From 68993e86490c12a6cbe185b6aa34d5b5b71e5420 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Fri, 28 Aug 2020 23:04:45 +0430 Subject: [PATCH 20/52] update --- src/Values/Config.php | 2 +- src/Values/Route.php | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Values/Config.php b/src/Values/Config.php index 70d86b7..ea74d81 100644 --- a/src/Values/Config.php +++ b/src/Values/Config.php @@ -13,7 +13,7 @@ class Config { /** - * URI prefix + * Path prefix * * @var string */ diff --git a/src/Values/Route.php b/src/Values/Route.php index e670a83..c7bbb1a 100644 --- a/src/Values/Route.php +++ b/src/Values/Route.php @@ -56,7 +56,7 @@ class Route * Route constructor. * * @param string|null $name - * @param string $uri + * @param string $path * @param string|null $method * @param Closure|callable|string $controller * @param string[]|callable[]|Closure[]|Middleware[] $middleware @@ -64,7 +64,7 @@ class Route */ public function __construct( ?string $name, - string $uri, + string $path, string $method, $controller, $middleware, @@ -72,7 +72,7 @@ public function __construct( ) { $this->name = $name; - $this->path = $uri; + $this->path = $path; $this->method = $method; $this->controller = $controller; $this->middleware = $middleware; @@ -86,11 +86,13 @@ public function toArray(): array { return [ 'name' => $this->name, - 'uri' => $this->path, + 'path' => $this->path, 'method' => $this->method, 'controller' => $this->controller, 'middleware' => $this->middleware, 'domain' => $this->domain, + 'uri' => $this->uri, + 'parameters' => $this->parameters, ]; } From acef13877bba8c00b799129a59dc1396938a3826 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 29 Aug 2020 00:29:54 +0430 Subject: [PATCH 21/52] add test for getting and setting container --- tests/ContainerTest.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/ContainerTest.php diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php new file mode 100644 index 0000000..0b2f035 --- /dev/null +++ b/tests/ContainerTest.php @@ -0,0 +1,30 @@ +singleton('name', 'Pink Floyd'); + + $router = new Router(); + $router->setPublisher(new FakePublisher()); + $router->setContainer($container); + + $router->get('/', function (Container $container) { + return $container->get('name'); + })->dispatch(); + + $this->assertEquals('Pink Floyd', $this->output($router)); + } +} From 37bfdb6fd7987116948c9aaef1179366e1d7b4c1 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 29 Aug 2020 00:33:54 +0430 Subject: [PATCH 22/52] add test for route uri and parameters --- src/Values/Route.php | 12 ++++++------ tests/RoutingTest.php | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Values/Route.php b/src/Values/Route.php index c7bbb1a..5a70946 100644 --- a/src/Values/Route.php +++ b/src/Values/Route.php @@ -48,9 +48,9 @@ class Route private $uri = null; /** - * @var null|string[]|array[string]string + * @var string[]|array[string]string */ - private $parameters = null; + private $parameters = []; /** * Route constructor. @@ -161,17 +161,17 @@ public function getDomain(): ?string } /** - * @return array|string[]|null + * @return array|string[] */ - public function getParameters(): ?array + public function getParameters(): array { return $this->parameters; } /** - * @param array|string[]|null $parameters + * @param array|string[] $parameters */ - public function setParameters(?array $parameters): void + public function setParameters(array $parameters): void { $this->parameters = $parameters; } diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 077e3a6..23258aa 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -428,6 +428,8 @@ public function test_current_route() return join(',', [ $r->currentRoute()->getName(), $r->currentRoute()->getPath(), + $r->currentRoute()->getUri(), + $r->currentRoute()->getParameters(), $r->currentRoute()->getMethod(), count($r->currentRoute()->getMiddleware()), $r->currentRoute()->getDomain() ?? '-', @@ -435,7 +437,7 @@ public function test_current_route() }, 'home') ->dispatch(); - $value = join(',', ['home', '/', 'GET', 0, '-']); + $value = join(',', ['home', '/','/', [], 'GET', 0, '-']); $this->assertEquals($value, $this->output($router)); } } From fa0fb4c24c85d9ae81103ba6adf4663c6f7d589b Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 29 Aug 2020 00:35:41 +0430 Subject: [PATCH 23/52] remove github action --- .github/workflows/php.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/php.yml diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml deleted file mode 100644 index a34418c..0000000 --- a/.github/workflows/php.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Run tests - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - - name: Validate composer.json and composer.lock - run: composer validate - - - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest - - # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" - # Docs: https://getcomposer.org/doc/articles/scripts.md - - - name: Run test suite - run: ./vendor/bin/phpunit ./tests From f32dfc088dc6e1e384e8047109e6974b01b93d14 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 21 Nov 2020 13:24:45 +0330 Subject: [PATCH 24/52] update --- composer.json | 84 ++-- src/Config.php | 96 ++++ src/Enums/GroupAttributes.php | 6 +- ...ption.php => InvalidCallableException.php} | 2 +- src/Exceptions/InvalidMiddlewareException.php | 15 - src/Middleware.php | 24 - src/{Values => }/Route.php | 19 +- src/Router.php | 434 +++++++----------- src/Values/Config.php | 42 -- tests/GroupingTest.php | 21 +- tests/MiddlewareTest.php | 57 +-- tests/NamingTest.php | 21 +- tests/RoutingTest.php | 58 +-- tests/TestCase.php | 2 +- tests/Testing/SampleMiddleware.php | 6 +- tests/Testing/StopperMiddleware.php | 3 +- 16 files changed, 363 insertions(+), 527 deletions(-) create mode 100644 src/Config.php rename src/Exceptions/{InvalidControllerException.php => InvalidCallableException.php} (77%) delete mode 100644 src/Exceptions/InvalidMiddlewareException.php delete mode 100644 src/Middleware.php rename src/{Values => }/Route.php (88%) delete mode 100644 src/Values/Config.php diff --git a/composer.json b/composer.json index 47a614d..7c2d99d 100644 --- a/composer.json +++ b/composer.json @@ -1,45 +1,45 @@ { - "name": "miladrahimi/phprouter", - "description": "A powerful, lightweight, and very fast HTTP URL router.", - "keywords": [ - "Route", - "Router", - "Routing", - "URL Router", - "HTTP Router", - "URL", - "HTTP" - ], - "homepage": "https://github.com/miladrahimi/phprouter", - "type": "library", - "license": "MIT", - "authors": [ - { - "name": "Milad Rahimi", - "email": "info@miladrahimi.com", - "homepage": "https://miladrahimi.com", - "role": "Developer" + "name": "miladrahimi/phprouter", + "description": "A powerful, lightweight, and very fast HTTP URL router.", + "keywords": [ + "Route", + "Router", + "Routing", + "URL Router", + "HTTP Router", + "URL", + "HTTP" + ], + "homepage": "https://github.com/miladrahimi/phprouter", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Milad Rahimi", + "email": "info@miladrahimi.com", + "homepage": "https://miladrahimi.com", + "role": "Developer" + } + ], + "support": { + "email": "info@miladrahimi.com", + "source": "https://github.com/miladrahimi/phprouter/issues" + }, + "require": { + "php": ">=7.1", + "ext-json": "*", + "ext-mbstring": "*", + "laminas/laminas-diactoros": "^2.2", + "miladrahimi/phpcontainer": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^7" + }, + "autoload": { + "psr-4": { + "MiladRahimi\\PhpRouter\\": "src/", + "MiladRahimi\\PhpRouter\\Tests\\": "tests/", + "MiladRahimi\\PhpRouter\\Examples\\": "examples/" + } } - ], - "support": { - "email": "info@miladrahimi.com", - "source": "https://github.com/miladrahimi/phprouter/issues" - }, - "require": { - "php": ">=7.1", - "ext-json": "*", - "ext-mbstring": "*", - "laminas/laminas-diactoros": "^2.2", - "miladrahimi/phpcontainer": "^4.2" - }, - "require-dev": { - "phpunit/phpunit": "^7" - }, - "autoload": { - "psr-4": { - "MiladRahimi\\PhpRouter\\": "src/", - "MiladRahimi\\PhpRouter\\Tests\\": "tests/", - "MiladRahimi\\PhpRouter\\Examples\\": "examples/" - } - } } diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..0ec2021 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,96 @@ +prefix; + } + + /** + * @param string $prefix + */ + public function setPrefix(string $prefix): void + { + $this->prefix = $prefix; + } + + /** + * @param string $prefix + */ + public function addPrefix(string $prefix): void + { + $this->prefix .= $prefix; + } + + /** + * @return string[]|callable[] + */ + public function getMiddleware(): array + { + return $this->middleware; + } + + /** + * @param string[]|callable[] $middleware + */ + public function setMiddleware(array $middleware): void + { + $this->middleware = $middleware; + } + + /** + * @param string[]|callable[] $middleware + */ + public function addMiddleware(array $middleware): void + { + $this->middleware = array_merge($this->middleware, $middleware); + } + + /** + * @return string|null + */ + public function getDomain(): ?string + { + return $this->domain; + } + + /** + * @param string|null $domain + */ + public function setDomain(?string $domain): void + { + $this->domain = $domain; + } +} diff --git a/src/Enums/GroupAttributes.php b/src/Enums/GroupAttributes.php index bc4e937..00988e0 100644 --- a/src/Enums/GroupAttributes.php +++ b/src/Enums/GroupAttributes.php @@ -9,8 +9,12 @@ */ class GroupAttributes { + // The group middleware list const MIDDLEWARE = 'middleware'; + + // The group url prefix const PREFIX = 'prefix'; - const NAMESPACE = 'namespace'; + + // The group domain (subdomain) const DOMAIN = 'domain'; } diff --git a/src/Exceptions/InvalidControllerException.php b/src/Exceptions/InvalidCallableException.php similarity index 77% rename from src/Exceptions/InvalidControllerException.php rename to src/Exceptions/InvalidCallableException.php index ed5837d..844d9dc 100644 --- a/src/Exceptions/InvalidControllerException.php +++ b/src/Exceptions/InvalidCallableException.php @@ -9,7 +9,7 @@ * * @package MiladRahimi\PhpRouter\Exceptions */ -class InvalidControllerException extends Exception +class InvalidCallableException extends Exception { // } diff --git a/src/Exceptions/InvalidMiddlewareException.php b/src/Exceptions/InvalidMiddlewareException.php deleted file mode 100644 index 421431c..0000000 --- a/src/Exceptions/InvalidMiddlewareException.php +++ /dev/null @@ -1,15 +0,0 @@ -middleware; } @@ -184,7 +183,7 @@ public function getUri(): ?string return $this->uri; } - /** + /** * @param string|null $uri */ public function setUri(?string $uri): void diff --git a/src/Router.php b/src/Router.php index 9a4d048..8827c58 100644 --- a/src/Router.php +++ b/src/Router.php @@ -8,32 +8,28 @@ use MiladRahimi\PhpContainer\Exceptions\NotFoundException; use MiladRahimi\PhpRouter\Enums\GroupAttributes; use MiladRahimi\PhpRouter\Enums\HttpMethods; -use MiladRahimi\PhpRouter\Exceptions\InvalidControllerException; -use MiladRahimi\PhpRouter\Exceptions\InvalidMiddlewareException; +use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; -use MiladRahimi\PhpRouter\Values\Route; -use MiladRahimi\PhpRouter\Values\Config; +use MiladRahimi\PhpRouter\Route; +use MiladRahimi\PhpRouter\Config; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Throwable; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; /** * Class Router - * Router is the main class in the package. - * It's responsible for defining and dispatching routes. * * @package MiladRahimi\PhpRouter */ class Router { /** - * List of defined routes + * List of declared routes * * @var Route[][]|array[string][int]Route */ @@ -56,16 +52,16 @@ class Router /** * User HTTP Request * - * @var ServerRequestInterface|null + * @var ServerRequestInterface */ - private $request = null; + private $request; /** * The publisher that is going to publish outputs of controllers * - * @var Publisher|null + * @var Publisher */ - private $publisher = null; + private $publisher; /** * The configuration of current instance/group @@ -76,14 +72,7 @@ class Router private $config; /** - * Current route that is recognized for current request - * - * @var Route|null - */ - private $currentRoute = null; - - /** - * The dependency injection container + * The dependency injection IoC container * * @var Container */ @@ -97,7 +86,10 @@ class Router public function __construct(?Config $config = null) { $this->config = $config ?: new Config(); + $this->container = new Container(); + $this->publisher = new HttpPublisher(); + $this->request = ServerRequestFactory::fromGlobals(); } /** @@ -114,26 +106,17 @@ public function group(array $attributes, Closure $body): self // Set middleware for the group if (isset($attributes[GroupAttributes::MIDDLEWARE])) { - if (is_array($attributes[GroupAttributes::MIDDLEWARE]) == false) { - $this->config->middleware[] = $attributes[GroupAttributes::MIDDLEWARE]; - } else { - $this->config->middleware = array_merge($config->middleware, $attributes[GroupAttributes::MIDDLEWARE]); - } - } - - // Set namespace for the group - if (isset($attributes[GroupAttributes::NAMESPACE])) { - $this->config->namespace = join("\\", [$config->namespace, $attributes[GroupAttributes::NAMESPACE]]); + $this->config->addMiddleware($attributes[GroupAttributes::MIDDLEWARE]); } // Set prefix for the group if (isset($attributes[GroupAttributes::PREFIX])) { - $this->config->prefix = $config->prefix . $attributes[GroupAttributes::PREFIX]; + $this->config->addPrefix($attributes[GroupAttributes::PREFIX]); } // Set domain for the group if (isset($attributes[GroupAttributes::DOMAIN])) { - $this->config->domain = $attributes[GroupAttributes::DOMAIN]; + $this->config->setDomain($attributes[GroupAttributes::DOMAIN]); } // Run the group body closure @@ -149,31 +132,20 @@ public function group(array $attributes, Closure $body): self * Map a controller to a route and set basic attributes * * @param string $method - * @param string $route - * @param Closure|callable|string $controller + * @param string $path + * @param Closure|array $controller * @param string|null $name * @return self */ - public function map( - string $method, - string $route, - $controller, - ?string $name = null - ): self + public function map(string $method, string $path, $controller, ?string $name = null): self { - $uri = $this->config->prefix . $route; - - if (is_string($controller) && is_callable($controller) == false) { - $controller = join("\\", [$this->config->namespace, $controller]); - } - $route = new Route( $name, - $uri, + $this->config->getPrefix() . $path, $method, $controller, - $this->config->middleware, - $this->config->domain + $this->config->getMiddleware(), + $this->config->getDomain() ); $this->routes[$method][] = $route; @@ -187,47 +159,27 @@ public function map( * Dispatch routes and run the application * * @return self + * @throws ContainerException + * @throws InvalidCallableException + * @throws NotFoundException * @throws RouteNotFoundException - * @throws InvalidControllerException - * @throws InvalidMiddlewareException - * @throws Throwable (the controller might throw any kind of exception) */ public function dispatch(): self { - $this->prepare(); - - $method = $this->request->getMethod(); $domain = $this->request->getUri()->getHost(); $uri = $this->request->getUri()->getPath(); - /** - * @var Route[] $routes - */ - $routes = array_merge( - $this->routes['*'] ?? [], - $this->routes[$method] ?? [] - ); - - sort($routes, SORT_DESC); - - foreach ($routes as $route) { + foreach ($this->routesForMethod($this->request->getMethod()) as $route) { $parameters = []; if ( (!$route->getDomain() || $this->compareDomain($route->getDomain(), $domain)) && $this->compareUri($route->getPath(), $uri, $parameters) ) { - foreach ($parameters as $key => $value) { - if (strstr($route->getPath(), (string)$key) === false) { - unset($parameters[$key]); - } - } - - $route->setParameters($parameters); + $route->setParameters($this->filterRouteParameters($parameters)); $route->setUri($uri); - $this->currentRoute = $route; - $this->publisher->publish($this->run($route, $parameters)); + $this->publisher->publish($this->run($route, $parameters, $this->request)); return $this; } @@ -237,28 +189,30 @@ public function dispatch(): self } /** - * Check if given request domain matches given route domain + * Get all candidate routes for current http method * - * @param string|null $routeDomain - * @param string $requestDomain - * @return bool + * @param string $method + * @return Route[] */ - private function compareDomain(?string $routeDomain, string $requestDomain): bool + private function routesForMethod(string $method): array { - return preg_match('@^' . $routeDomain . '$@', $requestDomain); + $routes = array_merge($this->routes['*'] ?? [], $this->routes[$method] ?? []); + sort($routes, SORT_DESC); + + return $routes; } /** - * Check if given request uri matches given uri method + * Filter route parameters and remove unnecessary parameters * - * @param string $path - * @param string $uri * @param array $parameters - * @return bool + * @return array */ - private function compareUri(string $path, string $uri, array &$parameters): bool + private function filterRouteParameters(array $parameters): array { - return preg_match('@^' . $this->regexUri($path) . '$@', $uri, $parameters); + return array_filter($parameters, function ($value, $name) { + return isset($value) && is_numeric($name) == false; + }, ARRAY_FILTER_USE_BOTH); } /** @@ -266,108 +220,136 @@ private function compareUri(string $path, string $uri, array &$parameters): bool * * @param Route $route * @param array $parameters + * @param ServerRequestInterface $request * @return ResponseInterface|mixed|null - * @throws InvalidControllerException - * @throws InvalidMiddlewareException - * @throws Throwable + * @throws ContainerException + * @throws InvalidCallableException + * @throws NotFoundException */ - private function run(Route $route, array $parameters) + private function run(Route $route, array $parameters, ServerRequestInterface $request) { - $controller = $route->getController(); + $this->container->singleton('$container', $this->container); + $this->container->singleton(Container::class, $this->container); + $this->container->singleton(ContainerInterface::class, $this->container); - if (count($middleware = $route->getMiddleware()) > 0) { - $controllerRunner = function (ServerRequest $request) use ($controller, $parameters) { - return $this->runController($controller, $parameters, $request); - }; + $this->container->singleton('$router', $this); + $this->container->singleton(Router::class, $this); + + $this->container->singleton('$route', $route); + $this->container->singleton(Route::class, $route); - return $this->runControllerThroughMiddleware($middleware, $this->request, $controllerRunner); + foreach ($parameters as $key => $value) { + $this->container->singleton('$' . $key, $value); } - return $this->runController($controller, $parameters, $this->request); + return $this->runStack(array_merge($route->getMiddleware(), [$route->getController()]), $request); } /** * Run the controller through the middleware (list) * - * @param string|callable|Closure|Middleware|string[]|callable[]|Closure[]|Middleware[] $middleware + * @param string[] $callables * @param ServerRequestInterface $request - * @param Closure $controllerRunner * @param int $i * @return ResponseInterface|mixed|null - * @throws InvalidMiddlewareException - */ - private function runControllerThroughMiddleware( - array $middleware, - ServerRequestInterface $request, - Closure $controllerRunner, - $i = 0 - ) + * @throws ContainerException + * @throws InvalidCallableException + * @throws NotFoundException + */ + private function runStack(array $callables, ServerRequestInterface $request, $i = 0) { - if (isset($middleware[$i + 1])) { - $next = function (ServerRequestInterface $request) use ($middleware, $controllerRunner, $i) { - return $this->runControllerThroughMiddleware($middleware, $request, $controllerRunner, $i + 1); - }; - } else { - $next = $controllerRunner; - } - - if (is_callable($middleware[$i])) { - return $middleware[$i]($request, $next); - } + $this->container->singleton('$request', $request); + $this->container->singleton(ServerRequest::class, $request); + $this->container->singleton(ServerRequestInterface::class, $request); - if (is_subclass_of($middleware[$i], Middleware::class)) { - if (is_string($middleware[$i])) { - $middleware[$i] = new $middleware[$i]; - } + if (isset($callables[$i + 1])) { + $next = function (ServerRequestInterface $request) use ($callables, $i) { + return $this->runStack($callables, $request, $i + 1); + }; - return $middleware[$i]->handle($request, $next); + $this->container->closure('$next', $next); } - throw new InvalidMiddlewareException('Invalid middleware for route: ' . $this->currentRoute); + return $this->runCallable($callables[$i]); } /** - * Run the controller + * Run the given callable (method, closure, etc.) * - * @param Closure|callable|string $controller - * @param array $parameters - * @param ServerRequestInterface $request + * @param Closure|callable|string $callable * @return ResponseInterface|mixed|null - * @throws InvalidControllerException * @throws ContainerException + * @throws InvalidCallableException * @throws NotFoundException */ - private function runController($controller, array $parameters, ServerRequestInterface $request) + private function runCallable($callable) { - if (is_string($controller) && strpos($controller, '@')) { - list($className, $methodName) = explode('@', $controller); + if (is_array($callable)) { + if (count($callable) != 2) { + throw new InvalidCallableException('Invalid callable: ' . implode(',', $callable)); + } - if (class_exists($className) == false) { - throw new InvalidControllerException("Controller class `$controller` not found."); + list($class, $method) = $callable; + + if (class_exists($class) == false) { + throw new InvalidCallableException("Class `$callable` not found."); } - $classObject = new $className(); + $object = new $class(); - if (method_exists($classObject, $methodName) == false) { - throw new InvalidControllerException("Controller method `$methodName` not found."); + if (method_exists($object, $method) == false) { + throw new InvalidCallableException("Method `$class::$method` not found."); + } + + $callable = [$object, $method]; + } else { + if (is_string($callable)) { + if (class_exists($callable)) { + $callable = new $callable(); + } else { + throw new InvalidCallableException("Class `$callable` not found."); + } } - $controller = [$classObject, $methodName]; + if (is_object($callable) && !($callable instanceof Closure)) { + if (method_exists($callable, 'handle')) { + $callable = [$callable, 'handle']; + } else { + throw new InvalidCallableException("Method `$callable::handle` not found."); + } + } } - if (is_callable($controller) == false) { - throw new InvalidControllerException('Invalid controller: ' . $controller); + if (is_callable($callable) == false) { + throw new InvalidCallableException('Invalid callable.'); } - $this->container->singleton('$request', $request); - $this->container->singleton(ServerRequest::class, $request); - $this->container->singleton(ServerRequestInterface::class, $request); + return $this->container->call($callable); + } - foreach ($parameters as $key => $value) { - $this->container->singleton('$' . $key, $value); - } + /** + * Check if given request domain matches given route domain + * + * @param string|null $routeDomain + * @param string $requestDomain + * @return bool + */ + private function compareDomain(?string $routeDomain, string $requestDomain): bool + { + return preg_match('@^' . $routeDomain . '$@', $requestDomain); + } - return $this->container->call($controller); + /** + * Check if given request uri matches given uri method + * + * @param string $path + * @param string $uri + * @param array $parameters + * @return bool + */ + private function compareUri(string $path, string $uri, array &$parameters): bool + { + return preg_match('@^' . $this->regexUri($path) . '$@', $uri, $parameters); } /** @@ -403,6 +385,46 @@ private function regexParameter(string $name): string return '(?<' . $name . '>' . $pattern . ')' . $suffix; } + /** + * Define a route parameter pattern + * + * @param string $name + * @param string $pattern + * @return self + */ + public function define(string $name, string $pattern): self + { + $this->parameters[$name] = $pattern; + + return $this; + } + + /** + * Generate URL for given route name + * + * @param string $route + * @param string[] $parameters + * @return string + * @throws UndefinedRouteException + */ + public function url(string $route, array $parameters = []): string + { + if (isset($this->names[$route]) == false) { + throw new UndefinedRouteException("There is no route with name `$route`."); + } + + $uri = $this->names[$route]->getPath(); + + foreach ($parameters as $name => $value) { + $uri = preg_replace('/\??{' . $name . '\??}/', $value, $uri); + } + + $uri = preg_replace('/{[^}]+\?}/', '', $uri); + $uri = str_replace('/?', '', $uri); + + return $uri; + } + /** * Map a controller to given route for all the http methods * @@ -411,11 +433,7 @@ private function regexParameter(string $name): string * @param string|null $name * @return self */ - public function any( - string $route, - $controller, - ?string $name = null - ): self + public function any(string $route, $controller, ?string $name = null): self { return $this->map('*', $route, $controller, $name); } @@ -428,11 +446,7 @@ public function any( * @param string|null $name * @return self */ - public function get( - string $route, - $controller, - ?string $name = null - ): self + public function get(string $route, $controller, ?string $name = null): self { return $this->map(HttpMethods::GET, $route, $controller, $name); } @@ -445,11 +459,7 @@ public function get( * @param string|null $name * @return self */ - public function post( - string $route, - $controller, - ?string $name = null - ): self + public function post(string $route, $controller, ?string $name = null): self { return $this->map(HttpMethods::POST, $route, $controller, $name); } @@ -462,11 +472,7 @@ public function post( * @param string|null $name * @return self */ - public function put( - string $route, - $controller, - ?string $name = null - ): self + public function put(string $route, $controller, ?string $name = null): self { return $this->map(HttpMethods::PUT, $route, $controller, $name); } @@ -479,11 +485,7 @@ public function put( * @param string|null $name * @return self */ - public function patch( - string $route, - $controller, - ?string $name = null - ): self + public function patch(string $route, $controller, ?string $name = null): self { return $this->map(HttpMethods::PATCH, $route, $controller, $name); } @@ -496,92 +498,20 @@ public function patch( * @param string|null $name * @return self */ - public function delete( - string $route, - $controller, - ?string $name = null - ): self + public function delete(string $route, $controller, ?string $name = null): self { return $this->map(HttpMethods::DELETE, $route, $controller, $name); } /** - * Define a route parameter pattern - * - * @param string $name - * @param string $pattern - * @return self + * @return ServerRequestInterface */ - public function define(string $name, string $pattern): self - { - $this->parameters[$name] = $pattern; - - return $this; - } - - /** - * Generate URL for given route name - * - * @param string $route - * @param string[] $parameters - * @return string - * @throws UndefinedRouteException - */ - public function url(string $route, array $parameters = []): string - { - if (isset($this->names[$route]) == false) { - throw new UndefinedRouteException("There is no route with name `$route`."); - } - - $uri = $this->names[$route]->getPath(); - - foreach ($parameters as $name => $value) { - $uri = preg_replace('/\??{' . $name . '\??}/', $value, $uri); - } - - $uri = preg_replace('/{[^}]+\?}/', '', $uri); - $uri = str_replace('/?', '', $uri); - - return $uri; - } - - /** - * @return Route|null - */ - public function currentRoute(): ?Route - { - return $this->currentRoute; - } - - /** - * Prepare router to dispatch routes - */ - private function prepare(): void - { - $this->request = $this->request ?: ServerRequestFactory::fromGlobals(); - $this->publisher = $this->publisher ?: new HttpPublisher(); - - $this->container->singleton('$router', $this); - $this->container->singleton(Router::class, $this); - - $this->container->singleton('$container', $this->container); - $this->container->singleton(Container::class, $this->container); - $this->container->singleton(ContainerInterface::class, $this->container); - } - - /** - * Get current http request instance - * - * @return ServerRequestInterface|null - */ - public function getRequest(): ?ServerRequestInterface + public function getRequest(): ServerRequestInterface { return $this->request; } /** - * Set my own http request instance - * * @param ServerRequestInterface $request */ public function setRequest(ServerRequestInterface $request): void @@ -605,14 +535,6 @@ public function setPublisher(Publisher $publisher): void $this->publisher = $publisher; } - /** - * @return Container - */ - public function getContainer(): Container - { - return $this->container; - } - /** * @param Container $container */ diff --git a/src/Values/Config.php b/src/Values/Config.php deleted file mode 100644 index ea74d81..0000000 --- a/src/Values/Config.php +++ /dev/null @@ -1,42 +0,0 @@ -router() - ->group(['middleware' => $middleware], function (Router $router) { + ->group(['middleware' => [$middleware]], function (Router $router) { $router->get('/', $this->OkController()); })->dispatch(); @@ -47,8 +47,8 @@ public function test_nested_groups_with_middleware() $group2Middleware = new SampleMiddleware(mt_rand(1, 9999999)); $router = $this->router() - ->group(['middleware' => $group1Middleware], function (Router $router) use ($group2Middleware) { - $router->group(['middleware' => $group2Middleware], function (Router $router) { + ->group(['middleware' => [$group1Middleware]], function (Router $router) use ($group2Middleware) { + $router->group(['middleware' => [$group2Middleware]], function (Router $router) { $router->get('/', $this->OkController()); }); })->dispatch(); @@ -90,21 +90,6 @@ public function test_nested_groups_with_prefix() $this->assertEquals('OK', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_with_namespace() - { - $namespace = 'MiladRahimi\PhpRouter\Tests\Testing'; - - $router = $this->router() - ->group(['namespace' => $namespace], function (Router $router) { - $router->get('/', 'SampleController@home'); - })->dispatch(); - - $this->assertEquals('Home', $this->output($router)); - } - /** * @throws Throwable */ diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index b8019ae..b321004 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -2,13 +2,11 @@ namespace MiladRahimi\PhpRouter\Tests; -use Closure; -use MiladRahimi\PhpRouter\Exceptions\InvalidMiddlewareException; +use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Tests\Testing\SampleMiddleware; use MiladRahimi\PhpRouter\Tests\Testing\StopperMiddleware; use Throwable; -use Laminas\Diactoros\ServerRequest; class MiddlewareTest extends TestCase { @@ -19,7 +17,7 @@ public function test_with_a_single_middleware_as_an_object() { $middleware = new SampleMiddleware(666); - $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $router = $this->router()->group(['middleware' => [$middleware]], function (Router $r) { $r->get('/', $this->OkController()); })->dispatch(); @@ -34,7 +32,7 @@ public function test_with_a_single_middleware_as_a_string() { $middleware = SampleMiddleware::class; - $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $router = $this->router()->group(['middleware' => [$middleware]], function (Router $r) { $r->get('/', $this->OkController()); })->dispatch(); @@ -42,24 +40,6 @@ public function test_with_a_single_middleware_as_a_string() $this->assertEquals('empty', SampleMiddleware::$output[0]); } - /** - * @throws Throwable - */ - public function test_with_a_single_middleware_as_a_closure() - { - $middleware = function (ServerRequest $request, Closure $next) { - return $next($request->withAttribute('Middleware', 666)); - }; - - $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { - $r->get('/', function (ServerRequest $request) { - return $request->getAttribute('Middleware'); - }); - })->dispatch(); - - $this->assertEquals('666', $this->output($router)); - } - /** * @throws Throwable */ @@ -67,7 +47,7 @@ public function test_with_a_stopper_middleware() { $middleware = new StopperMiddleware(666); - $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { + $router = $this->router()->group(['middleware' => [$middleware]], function (Router $r) { $r->get('/', $this->OkController()); })->dispatch(); @@ -75,39 +55,14 @@ public function test_with_a_stopper_middleware() $this->assertContains($middleware->content, StopperMiddleware::$output); } - /** - * @throws Throwable - */ - public function test_with_multiple_middleware() - { - $middleware = [ - function (ServerRequest $request, $next) { - $request = $request->withAttribute('a', 'It'); - return $next($request); - }, - function (ServerRequest $request, $next) { - $request = $request->withAttribute('b', 'works!'); - return $next($request); - }, - ]; - - $router = $this->router()->group(['middleware' => $middleware], function (Router $r) { - $r->get('/', function (ServerRequest $request) { - return $request->getAttribute('a') . ' ' . $request->getAttribute('b'); - }); - })->dispatch(); - - $this->assertEquals('It works!', $this->output($router)); - } - /** * @throws Throwable */ public function test_with_invalid_middleware() { - $this->expectException(InvalidMiddlewareException::class); + $this->expectException(InvalidCallableException::class); - $this->router()->group(['middleware' => 'UnknownMiddleware'], function (Router $r) { + $this->router()->group(['middleware' => ['UnknownMiddleware']], function (Router $r) { $r->get('/', $this->OkController()); })->dispatch(); } diff --git a/tests/NamingTest.php b/tests/NamingTest.php index b73ade0..e09e6d1 100644 --- a/tests/NamingTest.php +++ b/tests/NamingTest.php @@ -3,6 +3,7 @@ namespace MiladRahimi\PhpRouter\Tests; use MiladRahimi\PhpRouter\Enums\HttpMethods; +use MiladRahimi\PhpRouter\Route; use Throwable; class NamingTest extends TestCase @@ -13,23 +14,11 @@ class NamingTest extends TestCase public function test_a_named_route() { $router = $this->router() - ->get('/', $this->OkController(), 'Home') + ->get('/', function (Route $route) { + return $route->getName(); + }, 'home') ->dispatch(); - $this->assertEquals('OK', $this->output($router)); - $this->assertTrue($router->currentRoute()->getName() == 'Home'); - } - - /** - * @throws Throwable - */ - public function test_duplicate_naming_it_should_set_the_name_for_all_routes() - { - $router = $this->router() - ->get('/', $this->OkController(), 'Home') - ->get('/home', $this->OkController(), 'Home') - ->dispatch(); - - $this->assertTrue($router->currentRoute()->getName() == 'Home'); + $this->assertEquals('home', $this->output($router)); } } diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 23258aa..d39f408 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -3,10 +3,11 @@ namespace MiladRahimi\PhpRouter\Tests; use MiladRahimi\PhpRouter\Enums\HttpMethods; -use MiladRahimi\PhpRouter\Exceptions\InvalidControllerException; +use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Tests\Testing\SampleController; +use MiladRahimi\PhpRouter\Route; use Psr\Http\Message\ServerRequestInterface; use Throwable; use Laminas\Diactoros\ServerRequest; @@ -284,35 +285,6 @@ public function test_injection_of_request_by_type() $this->assertEquals('GET', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_injection_of_router_by_name() - { - $router = $this->router() - ->get('/', function ($router) { - /** @var Router $router */ - return $router->currentRoute()->getName(); - }, 'home') - ->dispatch(); - - $this->assertEquals('home', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_injection_of_router_by_type() - { - $router = $this->router() - ->get('/', function (Router $r) { - return $r->currentRoute()->getName(); - }, 'home') - ->dispatch(); - - $this->assertEquals('home', $this->output($router)); - } - /** * @throws Throwable */ @@ -367,10 +339,8 @@ public function test_default_publisher() */ public function test_with_fully_namespaced_controller() { - $c = 'MiladRahimi\PhpRouter\Tests\Testing\SampleController@home'; - $router = $this->router() - ->get('/', $c) + ->get('/', [SampleController::class, 'home']) ->dispatch(); $this->assertEquals('Home', $this->output($router)); @@ -393,7 +363,7 @@ public function test_not_found_error() */ public function test_with_class_method_but_invalid_controller_class() { - $this->expectException(InvalidControllerException::class); + $this->expectException(InvalidCallableException::class); $this->router()->get('/', 'UnknownController@method')->dispatch(); } @@ -403,7 +373,7 @@ public function test_with_class_method_but_invalid_controller_class() */ public function test_with_class_but_invalid_method() { - $this->expectException(InvalidControllerException::class); + $this->expectException(InvalidCallableException::class); $this->router()->get('/', SampleController::class . '@invalid')->dispatch(); } @@ -413,7 +383,7 @@ public function test_with_class_but_invalid_method() */ public function test_with_invalid_controller_class() { - $this->expectException(InvalidControllerException::class); + $this->expectException(InvalidCallableException::class); $this->router()->get('/', 666)->dispatch(); } @@ -424,15 +394,15 @@ public function test_with_invalid_controller_class() public function test_current_route() { $router = $this->router() - ->get('/', function (Router $r) { + ->get('/', function (Route $r) { return join(',', [ - $r->currentRoute()->getName(), - $r->currentRoute()->getPath(), - $r->currentRoute()->getUri(), - $r->currentRoute()->getParameters(), - $r->currentRoute()->getMethod(), - count($r->currentRoute()->getMiddleware()), - $r->currentRoute()->getDomain() ?? '-', + $r->getName(), + $r->getPath(), + $r->getUri(), + $r->getParameters(), + $r->getMethod(), + count($r->getMiddleware()), + $r->getDomain() ?? '-', ]); }, 'home') ->dispatch(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 4caf630..12cae5e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,7 +7,7 @@ use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; -use MiladRahimi\PhpRouter\Values\Config; +use MiladRahimi\PhpRouter\Config; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase diff --git a/tests/Testing/SampleMiddleware.php b/tests/Testing/SampleMiddleware.php index 8412fb7..46f039f 100644 --- a/tests/Testing/SampleMiddleware.php +++ b/tests/Testing/SampleMiddleware.php @@ -2,11 +2,9 @@ namespace MiladRahimi\PhpRouter\Tests\Testing; -use Closure; -use MiladRahimi\PhpRouter\Middleware; use Psr\Http\Message\ServerRequestInterface; -class SampleMiddleware implements Middleware +class SampleMiddleware { /** * @var string @@ -33,7 +31,7 @@ public function __construct(string $content = null) /** * @inheritdoc */ - public function handle(ServerRequestInterface $request, Closure $next) + public function handle(ServerRequestInterface $request, $next) { static::$output[] = $this->content; diff --git a/tests/Testing/StopperMiddleware.php b/tests/Testing/StopperMiddleware.php index e511041..4b993e3 100644 --- a/tests/Testing/StopperMiddleware.php +++ b/tests/Testing/StopperMiddleware.php @@ -3,11 +3,10 @@ namespace MiladRahimi\PhpRouter\Tests\Testing; use Closure; -use MiladRahimi\PhpRouter\Middleware; use Psr\Http\Message\ServerRequestInterface; use Laminas\Diactoros\Response\TextResponse; -class StopperMiddleware implements Middleware +class StopperMiddleware { /** * @var string From 537aa1f279144a32ef158826d1c1bba516730a0f Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Wed, 25 Nov 2020 20:26:48 +0330 Subject: [PATCH 25/52] v5 --- src/Config.php | 6 +++-- src/Route.php | 26 ++++++++++---------- src/Router.php | 64 ++++++++++++++++++++++++-------------------------- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/Config.php b/src/Config.php index 0ec2021..0ac1c88 100644 --- a/src/Config.php +++ b/src/Config.php @@ -2,10 +2,12 @@ namespace MiladRahimi\PhpRouter; +use Closure; + /** * Class Config * - * @package MiladRahimi\PhpRouter\Values + * @package MiladRahimi\PhpRouter */ class Config { @@ -19,7 +21,7 @@ class Config /** * Middleware list * - * @var string[]|callable[] + * @var array[]|Closure[] */ private $middleware = []; diff --git a/src/Route.php b/src/Route.php index ce4a0e5..3dc88f2 100644 --- a/src/Route.php +++ b/src/Route.php @@ -27,12 +27,12 @@ class Route private $method; /** - * @var Closure|array + * @var array|Closure */ private $controller; /** - * @var string[]|callable[] + * @var array[]|Closure[] */ private $middleware; @@ -42,12 +42,12 @@ class Route private $domain; /** - * @var null|string + * @var string */ private $uri = null; /** - * @var string[]|array[string]string + * @var string[] */ private $parameters = []; @@ -128,7 +128,7 @@ public function getPath(): string } /** - * @return string|null + * @return string */ public function getMethod(): string { @@ -136,7 +136,7 @@ public function getMethod(): string } /** - * @return Closure|callable|string + * @return array|Closure */ public function getController() { @@ -144,7 +144,7 @@ public function getController() } /** - * @return string[]|callable[] + * @return array[]|Closure[] */ public function getMiddleware(): array { @@ -160,7 +160,7 @@ public function getDomain(): ?string } /** - * @return array|string[] + * @return string[] */ public function getParameters(): array { @@ -168,7 +168,7 @@ public function getParameters(): array } /** - * @param array|string[] $parameters + * @param string[] $parameters */ public function setParameters(array $parameters): void { @@ -176,17 +176,17 @@ public function setParameters(array $parameters): void } /** - * @return string|null + * @return string */ - public function getUri(): ?string + public function getUri(): string { return $this->uri; } /** - * @param string|null $uri + * @param string $uri */ - public function setUri(?string $uri): void + public function setUri(string $uri): void { $this->uri = $uri; } diff --git a/src/Router.php b/src/Router.php index 8827c58..01d682b 100644 --- a/src/Router.php +++ b/src/Router.php @@ -13,9 +13,6 @@ use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; -use MiladRahimi\PhpRouter\Route; -use MiladRahimi\PhpRouter\Config; -use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Laminas\Diactoros\ServerRequest; @@ -31,21 +28,21 @@ class Router /** * List of declared routes * - * @var Route[][]|array[string][int]Route + * @var Route[][] */ private $routes = []; /** * List of named routes * - * @var Route[]|array[string]Route + * @var Route[] */ private $names = []; /** * List of defined route parameters * - * @var string[]|array[string]string + * @var string[] */ private $parameters = []; @@ -65,7 +62,7 @@ class Router /** * The configuration of current instance/group - * It holds current attributes like prefix, domain... + * It holds current attributes like prefix, domain, etc. * * @var Config */ @@ -169,14 +166,14 @@ public function dispatch(): self $domain = $this->request->getUri()->getHost(); $uri = $this->request->getUri()->getPath(); - foreach ($this->routesForMethod($this->request->getMethod()) as $route) { + foreach ($this->findRoutesByMethod($this->request->getMethod()) as $route) { $parameters = []; if ( (!$route->getDomain() || $this->compareDomain($route->getDomain(), $domain)) && $this->compareUri($route->getPath(), $uri, $parameters) ) { - $route->setParameters($this->filterRouteParameters($parameters)); + $route->setParameters($this->pruneRouteParameters($parameters)); $route->setUri($uri); $this->publisher->publish($this->run($route, $parameters, $this->request)); @@ -189,12 +186,12 @@ public function dispatch(): self } /** - * Get all candidate routes for current http method + * Find all candidate routes for current http method * * @param string $method * @return Route[] */ - private function routesForMethod(string $method): array + private function findRoutesByMethod(string $method): array { $routes = array_merge($this->routes['*'] ?? [], $this->routes[$method] ?? []); sort($routes, SORT_DESC); @@ -203,15 +200,16 @@ private function routesForMethod(string $method): array } /** - * Filter route parameters and remove unnecessary parameters + * Prune route parameters (remove unnecessary parameters) * * @param array $parameters * @return array + * @noinspection PhpUnusedParameterInspection */ - private function filterRouteParameters(array $parameters): array + private function pruneRouteParameters(array $parameters): array { return array_filter($parameters, function ($value, $name) { - return isset($value) && is_numeric($name) == false; + return is_numeric($name) == false; }, ARRAY_FILTER_USE_BOTH); } @@ -274,7 +272,7 @@ private function runStack(array $callables, ServerRequestInterface $request, $i } /** - * Run the given callable (method, closure, etc.) + * Run the given callable (controller or middleware) * * @param Closure|callable|string $callable * @return ResponseInterface|mixed|null @@ -428,79 +426,79 @@ public function url(string $route, array $parameters = []): string /** * Map a controller to given route for all the http methods * - * @param string $route + * @param string $path * @param Closure|callable|string $controller * @param string|null $name * @return self */ - public function any(string $route, $controller, ?string $name = null): self + public function any(string $path, $controller, ?string $name = null): self { - return $this->map('*', $route, $controller, $name); + return $this->map('*', $path, $controller, $name); } /** * Map a controller to given GET route * - * @param string $route + * @param string $path * @param Closure|callable|string $controller * @param string|null $name * @return self */ - public function get(string $route, $controller, ?string $name = null): self + public function get(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::GET, $route, $controller, $name); + return $this->map(HttpMethods::GET, $path, $controller, $name); } /** * Map a controller to given POST route * - * @param string $route + * @param string $path * @param Closure|callable|string $controller * @param string|null $name * @return self */ - public function post(string $route, $controller, ?string $name = null): self + public function post(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::POST, $route, $controller, $name); + return $this->map(HttpMethods::POST, $path, $controller, $name); } /** * Map a controller to given PUT route * - * @param string $route + * @param string $path * @param Closure|callable|string $controller * @param string|null $name * @return self */ - public function put(string $route, $controller, ?string $name = null): self + public function put(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::PUT, $route, $controller, $name); + return $this->map(HttpMethods::PUT, $path, $controller, $name); } /** * Map a controller to given PATCH route * - * @param string $route + * @param string $path * @param Closure|callable|string $controller * @param string|null $name * @return self */ - public function patch(string $route, $controller, ?string $name = null): self + public function patch(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::PATCH, $route, $controller, $name); + return $this->map(HttpMethods::PATCH, $path, $controller, $name); } /** * Map a controller to given DELETE route * - * @param string $route + * @param string $path * @param Closure|callable|string $controller * @param string|null $name * @return self */ - public function delete(string $route, $controller, ?string $name = null): self + public function delete(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::DELETE, $route, $controller, $name); + return $this->map(HttpMethods::DELETE, $path, $controller, $name); } /** From 2aa98d317ce189d8ad9105027160d20172a22242 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Thu, 26 Nov 2020 01:54:56 +0330 Subject: [PATCH 26/52] v5 --- README.md | 2 +- examples/example-10/index.php | 2 +- src/Attributes.php | 17 ++ src/Collection.php | 120 ++++++++++++++ src/Config.php | 98 ----------- src/Enums/GroupAttributes.php | 20 --- src/Enums/HttpMethods.php | 28 ---- src/Router.php | 285 +++++++++++++------------------- tests/ContainerTest.php | 8 +- tests/Enums/HttpMethodsTest.php | 30 ---- tests/GroupingTest.php | 9 +- tests/RoutingTest.php | 52 ++---- tests/TestCase.php | 15 +- tests/UrlTest.php | 11 +- 14 files changed, 294 insertions(+), 403 deletions(-) create mode 100644 src/Attributes.php create mode 100644 src/Collection.php delete mode 100644 src/Config.php delete mode 100644 src/Enums/GroupAttributes.php delete mode 100644 src/Enums/HttpMethods.php delete mode 100644 tests/Enums/HttpMethodsTest.php diff --git a/README.md b/README.md index f5e6924..8a420f6 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ use MiladRahimi\PhpRouter\Router; $router = new Router(); -$router->define('id', '[0-9]+'); +$router->patterns('id', '[0-9]+'); $router->get('/blog/post/{id}', function (int $id) { return 'Content of the post: ' . $id; diff --git a/examples/example-10/index.php b/examples/example-10/index.php index 5c09ad6..c238395 100644 --- a/examples/example-10/index.php +++ b/examples/example-10/index.php @@ -6,7 +6,7 @@ $router = new Router(); -$router->define('id', '[0-9]+'); +$router->patterns('id', '[0-9]+'); $router->get('/post/{id}', function (int $id) { return 'Content of the post: ' . $id; diff --git a/src/Attributes.php b/src/Attributes.php new file mode 100644 index 0000000..a100c87 --- /dev/null +++ b/src/Attributes.php @@ -0,0 +1,17 @@ + '', + Attributes::MIDDLEWARE => [], + Attributes::DOMAIN => null, + ]; + + /** + * Add a route to the collection + * + * @param string $method + * @param string $path + * @param $controller + * @param string|null $name + */ + public function add(string $method, string $path, $controller, ?string $name = null): void + { + $path = $this->attributes[Attributes::PREFIX] . $path; + $route = new Route( + $name, + $path, + $method, + $controller, + $this->attributes[Attributes::MIDDLEWARE], + $this->attributes[Attributes::DOMAIN] + ); + + $this->routes[$method][] = $route; + + $name && $this->names[$name] = $route; + } + + /** + * Find routes by given method + * + * @param string $method + * @return Route[] + */ + public function findByMethod(string $method): array + { + $routes = array_merge($this->routes['*'] ?? [], $this->routes[$method] ?? []); + + sort($routes, SORT_DESC); + + return $routes; + } + + /** + * Find route by given name + * + * @param string $name + * @return Route|null + */ + public function findByName(string $name): ?Route + { + return $this->names[$name] ?? null; + } + + /** + * @return Route[] + */ + public function getRoutes(): array + { + return $this->routes; + } + + /** + * @return Route[] + */ + public function getNames(): array + { + return $this->names; + } + + public function appendAttributes(array $newAttributes): void + { + $this->attributes[Attributes::PREFIX] .= $newAttributes[Attributes::PREFIX] ?? ''; + $this->attributes[Attributes::DOMAIN] = $newAttributes[Attributes::DOMAIN] ?? null; + $this->attributes[Attributes::MIDDLEWARE] = array_merge( + $this->attributes[Attributes::MIDDLEWARE], + $newAttributes[Attributes::MIDDLEWARE] ?? [] + ); + } + + /** + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @param array $attributes + */ + public function setAttributes(array $attributes): void + { + $this->attributes = $attributes; + } +} diff --git a/src/Config.php b/src/Config.php deleted file mode 100644 index 0ac1c88..0000000 --- a/src/Config.php +++ /dev/null @@ -1,98 +0,0 @@ -prefix; - } - - /** - * @param string $prefix - */ - public function setPrefix(string $prefix): void - { - $this->prefix = $prefix; - } - - /** - * @param string $prefix - */ - public function addPrefix(string $prefix): void - { - $this->prefix .= $prefix; - } - - /** - * @return string[]|callable[] - */ - public function getMiddleware(): array - { - return $this->middleware; - } - - /** - * @param string[]|callable[] $middleware - */ - public function setMiddleware(array $middleware): void - { - $this->middleware = $middleware; - } - - /** - * @param string[]|callable[] $middleware - */ - public function addMiddleware(array $middleware): void - { - $this->middleware = array_merge($this->middleware, $middleware); - } - - /** - * @return string|null - */ - public function getDomain(): ?string - { - return $this->domain; - } - - /** - * @param string|null $domain - */ - public function setDomain(?string $domain): void - { - $this->domain = $domain; - } -} diff --git a/src/Enums/GroupAttributes.php b/src/Enums/GroupAttributes.php deleted file mode 100644 index 00988e0..0000000 --- a/src/Enums/GroupAttributes.php +++ /dev/null @@ -1,20 +0,0 @@ -config = $config ?: new Config(); - + $this->collection = new Collection(); $this->container = new Container(); - $this->publisher = new HttpPublisher(); - $this->request = ServerRequestFactory::fromGlobals(); + + $this->bind([Router::class], $this); + $this->bind([Container::class, ContainerInterface::class], $this->container); + $this->bind([Publisher::class], HttpPublisher::class); + $this->bind([ServerRequestInterface::class, ServerRequest::class], ServerRequestFactory::fromGlobals()); } /** @@ -98,35 +66,19 @@ public function __construct(?Config $config = null) */ public function group(array $attributes, Closure $body): self { - // Backup group config - $config = clone $this->config; - - // Set middleware for the group - if (isset($attributes[GroupAttributes::MIDDLEWARE])) { - $this->config->addMiddleware($attributes[GroupAttributes::MIDDLEWARE]); - } + $oldAttributes = $this->collection->getAttributes(); + $this->collection->appendAttributes($attributes); - // Set prefix for the group - if (isset($attributes[GroupAttributes::PREFIX])) { - $this->config->addPrefix($attributes[GroupAttributes::PREFIX]); - } - - // Set domain for the group - if (isset($attributes[GroupAttributes::DOMAIN])) { - $this->config->setDomain($attributes[GroupAttributes::DOMAIN]); - } - - // Run the group body closure call_user_func($body, $this); // Revert to the old config - $this->config = $config; + $this->collection->setAttributes($oldAttributes); return $this; } /** - * Map a controller to a route and set basic attributes + * Map a controller to a route * * @param string $method * @param string $path @@ -136,18 +88,7 @@ public function group(array $attributes, Closure $body): self */ public function map(string $method, string $path, $controller, ?string $name = null): self { - $route = new Route( - $name, - $this->config->getPrefix() . $path, - $method, - $controller, - $this->config->getMiddleware(), - $this->config->getDomain() - ); - - $this->routes[$method][] = $route; - - $name && $this->names[$name] = $route; + $this->collection->add($method, $path, $controller, $name); return $this; } @@ -163,20 +104,13 @@ public function map(string $method, string $path, $controller, ?string $name = n */ public function dispatch(): self { - $domain = $this->request->getUri()->getHost(); - $uri = $this->request->getUri()->getPath(); + /** @var ServerRequestInterface $request */ + $request = $this->container->get(ServerRequestInterface::class); - foreach ($this->findRoutesByMethod($this->request->getMethod()) as $route) { + foreach ($this->collection->findByMethod($request->getMethod()) as $route) { $parameters = []; - - if ( - (!$route->getDomain() || $this->compareDomain($route->getDomain(), $domain)) && - $this->compareUri($route->getPath(), $uri, $parameters) - ) { - $route->setParameters($this->pruneRouteParameters($parameters)); - $route->setUri($uri); - - $this->publisher->publish($this->run($route, $parameters, $this->request)); + if ($this->compare($route, $request, $parameters)) { + $this->run($route, $parameters, $request); return $this; } @@ -186,17 +120,32 @@ public function dispatch(): self } /** - * Find all candidate routes for current http method + * Run the route controllers and related middleware * - * @param string $method - * @return Route[] + * @param Route $route + * @param array $parameters + * @param ServerRequestInterface $request + * @throws ContainerException + * @throws InvalidCallableException + * @throws NotFoundException */ - private function findRoutesByMethod(string $method): array + private function run(Route $route, array $parameters, ServerRequestInterface $request) { - $routes = array_merge($this->routes['*'] ?? [], $this->routes[$method] ?? []); - sort($routes, SORT_DESC); + $route->setParameters($this->pruneRouteParameters($parameters)); + $route->setUri($request->getUri()->getPath()); - return $routes; + $this->bind([Route::class], $route); + + foreach ($parameters as $key => $value) { + $this->bind(['$' . $key], $value); + } + + /** @var Publisher $publisher */ + $publisher = $this->container->get(Publisher::class); + $publisher->publish($this->callStack( + array_merge($route->getMiddleware(), [$route->getController()]), + $request + )); } /** @@ -214,37 +163,7 @@ private function pruneRouteParameters(array $parameters): array } /** - * Run the controller of the given route - * - * @param Route $route - * @param array $parameters - * @param ServerRequestInterface $request - * @return ResponseInterface|mixed|null - * @throws ContainerException - * @throws InvalidCallableException - * @throws NotFoundException - */ - private function run(Route $route, array $parameters, ServerRequestInterface $request) - { - $this->container->singleton('$container', $this->container); - $this->container->singleton(Container::class, $this->container); - $this->container->singleton(ContainerInterface::class, $this->container); - - $this->container->singleton('$router', $this); - $this->container->singleton(Router::class, $this); - - $this->container->singleton('$route', $route); - $this->container->singleton(Route::class, $route); - - foreach ($parameters as $key => $value) { - $this->container->singleton('$' . $key, $value); - } - - return $this->runStack(array_merge($route->getMiddleware(), [$route->getController()]), $request); - } - - /** - * Run the controller through the middleware (list) + * Call the given callable stack * * @param string[] $callables * @param ServerRequestInterface $request @@ -254,7 +173,7 @@ private function run(Route $route, array $parameters, ServerRequestInterface $re * @throws InvalidCallableException * @throws NotFoundException */ - private function runStack(array $callables, ServerRequestInterface $request, $i = 0) + private function callStack(array $callables, ServerRequestInterface $request, $i = 0) { $this->container->singleton('$request', $request); $this->container->singleton(ServerRequest::class, $request); @@ -262,7 +181,7 @@ private function runStack(array $callables, ServerRequestInterface $request, $i if (isset($callables[$i + 1])) { $next = function (ServerRequestInterface $request) use ($callables, $i) { - return $this->runStack($callables, $request, $i + 1); + return $this->callStack($callables, $request, $i + 1); }; $this->container->closure('$next', $next); @@ -325,6 +244,22 @@ private function runCallable($callable) return $this->container->call($callable); } + /** + * Compare given route with the given http request + * + * @param Route $route + * @param ServerRequestInterface $request + * @param array $parameters + * @return bool + */ + private function compare(Route $route, ServerRequestInterface $request, array &$parameters): bool + { + return ( + $this->compareDomain($route->getDomain(), $request->getUri()->getHost()) && + $this->compareUri($route->getPath(), $request->getUri()->getPath(), $parameters) + ); + } + /** * Check if given request domain matches given route domain * @@ -334,7 +269,7 @@ private function runCallable($callable) */ private function compareDomain(?string $routeDomain, string $requestDomain): bool { - return preg_match('@^' . $routeDomain . '$@', $requestDomain); + return !$routeDomain || preg_match('@^' . $routeDomain . '$@', $requestDomain); } /** @@ -378,7 +313,7 @@ private function regexParameter(string $name): string $suffix = ''; } - $pattern = $this->parameters[$name] ?? '[^/]+'; + $pattern = $this->patterns[$name] ?? '[^/]+'; return '(?<' . $name . '>' . $pattern . ')' . $suffix; } @@ -390,9 +325,9 @@ private function regexParameter(string $name): string * @param string $pattern * @return self */ - public function define(string $name, string $pattern): self + public function patterns(string $name, string $pattern): self { - $this->parameters[$name] = $pattern; + $this->patterns[$name] = $pattern; return $this; } @@ -400,18 +335,18 @@ public function define(string $name, string $pattern): self /** * Generate URL for given route name * - * @param string $route + * @param string $routeName * @param string[] $parameters * @return string * @throws UndefinedRouteException */ - public function url(string $route, array $parameters = []): string + public function url(string $routeName, array $parameters = []): string { - if (isset($this->names[$route]) == false) { - throw new UndefinedRouteException("There is no route with name `$route`."); + if (!($route = $this->collection->findByName($routeName))) { + throw new UndefinedRouteException("There is no route with name `$routeName`."); } - $uri = $this->names[$route]->getPath(); + $uri = $route->getPath(); foreach ($parameters as $name => $value) { $uri = preg_replace('/\??{' . $name . '\??}/', $value, $uri); @@ -423,6 +358,35 @@ public function url(string $route, array $parameters = []): string return $uri; } + /** + * Bind dependencies using the container + * + * @param array $abstracts + * @param $concrete + */ + private function bind(array $abstracts, $concrete): void + { + foreach ($abstracts as $abstract) $this->container->singleton($abstract, $concrete); + } + + /** + * Map a controller to given route with multiple http methods + * + * @param array $methods + * @param string $path + * @param Closure|callable|string $controller + * @param string|null $name + * @return self + */ + public function match(array $methods, string $path, $controller, ?string $name = null): self + { + foreach ($methods as $method) { + return $this->map($method, $path, $controller, $name); + } + + return $this; + } + /** * Map a controller to given route for all the http methods * @@ -446,7 +410,7 @@ public function any(string $path, $controller, ?string $name = null): self */ public function get(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::GET, $path, $controller, $name); + return $this->map('GET', $path, $controller, $name); } /** @@ -459,7 +423,7 @@ public function get(string $path, $controller, ?string $name = null): self */ public function post(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::POST, $path, $controller, $name); + return $this->map('POST', $path, $controller, $name); } /** @@ -472,7 +436,7 @@ public function post(string $path, $controller, ?string $name = null): self */ public function put(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::PUT, $path, $controller, $name); + return $this->map('PUT', $path, $controller, $name); } /** @@ -485,7 +449,7 @@ public function put(string $path, $controller, ?string $name = null): self */ public function patch(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::PATCH, $path, $controller, $name); + return $this->map('PATCH', $path, $controller, $name); } /** @@ -498,46 +462,35 @@ public function patch(string $path, $controller, ?string $name = null): self */ public function delete(string $path, $controller, ?string $name = null): self { - return $this->map(HttpMethods::DELETE, $path, $controller, $name); + return $this->map('DELETE', $path, $controller, $name); } /** - * @return ServerRequestInterface - */ - public function getRequest(): ServerRequestInterface - { - return $this->request; - } - - /** - * @param ServerRequestInterface $request - */ - public function setRequest(ServerRequestInterface $request): void - { - $this->request = $request; - } - - /** - * @return Publisher|null + * Map a controller to given OPTIONS route + * + * @param string $path + * @param Closure|callable|string $controller + * @param string|null $name + * @return self */ - public function getPublisher(): ?Publisher + public function options(string $path, $controller, ?string $name = null): self { - return $this->publisher; + return $this->map('OPTIONS', $path, $controller, $name); } /** - * @param Publisher $publisher + * @param Container $container */ - public function setPublisher(Publisher $publisher): void + public function setContainer(Container $container): void { - $this->publisher = $publisher; + $this->container = $container; } /** - * @param Container $container + * @return Container */ - public function setContainer(Container $container): void + public function getContainer(): Container { - $this->container = $container; + return $this->container; } } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 0b2f035..83df3aa 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -4,6 +4,7 @@ use MiladRahimi\PhpContainer\Container; use MiladRahimi\PhpRouter\Router; +use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; use Throwable; @@ -14,12 +15,9 @@ class ContainerTest extends TestCase */ public function test_setting_and_getting_container() { - $container = new Container(); - $container->singleton('name', 'Pink Floyd'); - $router = new Router(); - $router->setPublisher(new FakePublisher()); - $router->setContainer($container); + $router->getContainer()->singleton('name', 'Pink Floyd');; + $router->getContainer()->singleton(Publisher::class, new FakePublisher());; $router->get('/', function (Container $container) { return $container->get('name'); diff --git a/tests/Enums/HttpMethodsTest.php b/tests/Enums/HttpMethodsTest.php deleted file mode 100644 index d4ed6d5..0000000 --- a/tests/Enums/HttpMethodsTest.php +++ /dev/null @@ -1,30 +0,0 @@ -assertEquals('APPEND', $actual); - } - - public function test_custom_method_with_uppercase_method_it_should_standardize_it() - { - $actual = HttpMethods::custom('APPEND'); - - $this->assertEquals('APPEND', $actual); - } - - public function test_custom_method_with_mixed_case_method_it_should_standardize_it() - { - $actual = HttpMethods::custom('AppEnd'); - - $this->assertEquals('APPEND', $actual); - } -} diff --git a/tests/GroupingTest.php b/tests/GroupingTest.php index 6ddee58..9c9490e 100644 --- a/tests/GroupingTest.php +++ b/tests/GroupingTest.php @@ -2,7 +2,6 @@ namespace MiladRahimi\PhpRouter\Tests; -use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Tests\Testing\SampleMiddleware; use Throwable; @@ -63,7 +62,7 @@ public function test_nested_groups_with_middleware() */ public function test_with_a_prefix() { - $this->mockRequest(HttpMethods::GET, 'http://example.com/group/page'); + $this->mockRequest('GET', 'http://example.com/group/page'); $router = $this->router() ->group(['prefix' => '/group'], function (Router $router) { @@ -78,7 +77,7 @@ public function test_with_a_prefix() */ public function test_nested_groups_with_prefix() { - $this->mockRequest(HttpMethods::GET, 'http://example.com/group1/group2/page'); + $this->mockRequest('GET', 'http://example.com/group1/group2/page'); $router = $this->router() ->group(['prefix' => '/group1'], function (Router $router) { @@ -95,7 +94,7 @@ public function test_nested_groups_with_prefix() */ public function test_with_domain() { - $this->mockRequest(HttpMethods::GET, 'http://sub.domain.tld/'); + $this->mockRequest('GET', 'http://sub.domain.tld/'); $router = $this->router() ->group(['domain' => 'sub.domain.tld'], function (Router $router) { @@ -110,7 +109,7 @@ public function test_with_domain() */ public function test_nested_groups_with_domain_it_should_consider_the_inner_group_domain() { - $this->mockRequest(HttpMethods::GET, 'http://sub2.domain.com/'); + $this->mockRequest('GET', 'http://sub2.domain.com/'); $router = $this->router() ->group(['domain' => 'sub1.domain.com'], function (Router $router) { diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index d39f408..5910b8f 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -2,7 +2,6 @@ namespace MiladRahimi\PhpRouter\Tests; -use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Router; @@ -19,7 +18,7 @@ class RoutingTest extends TestCase */ public function test_a_simple_get_route() { - $this->mockRequest(HttpMethods::GET, 'http://example.com/'); + $this->mockRequest('GET', 'http://example.com/'); $router = $this->router()->map('GET', '/', $this->OkController())->dispatch(); @@ -31,7 +30,7 @@ public function test_a_simple_get_route() */ public function test_a_simple_post_route() { - $this->mockRequest(HttpMethods::POST, 'http://example.com/'); + $this->mockRequest('POST', 'http://example.com/'); $router = $this->router()->post('/', $this->OkController())->dispatch(); @@ -43,7 +42,7 @@ public function test_a_simple_post_route() */ public function test_a_simple_put_route() { - $this->mockRequest(HttpMethods::PUT, 'http://example.com/'); + $this->mockRequest('PUT', 'http://example.com/'); $router = $this->router()->put('/', $this->OkController())->dispatch(); @@ -55,7 +54,7 @@ public function test_a_simple_put_route() */ public function test_a_simple_patch_route() { - $this->mockRequest(HttpMethods::PATCH, 'http://example.com/'); + $this->mockRequest('PATCH', 'http://example.com/'); $router = $this->router()->patch('/', $this->OkController())->dispatch(); @@ -67,7 +66,7 @@ public function test_a_simple_patch_route() */ public function test_a_simple_delete_route() { - $this->mockRequest(HttpMethods::DELETE, 'http://example.com/'); + $this->mockRequest('DELETE', 'http://example.com/'); $router = $this->router()->delete('/', $this->OkController())->dispatch(); @@ -93,7 +92,7 @@ public function test_a_route_with_custom_method() */ public function test_the_any_method() { - $this->mockRequest(HttpMethods::GET, 'http://example.com/'); + $this->mockRequest('GET', 'http://example.com/'); $router = $this->router()->any('/', function () { return 'Test any for get'; @@ -101,7 +100,7 @@ public function test_the_any_method() $this->assertEquals('Test any for get', $this->output($router)); - $this->mockRequest(HttpMethods::POST, 'http://example.com/'); + $this->mockRequest('POST', 'http://example.com/'); $router = $this->router() ->any('/', function () { @@ -117,7 +116,7 @@ public function test_the_any_method() */ public function test_multiple_routes() { - $this->mockRequest(HttpMethods::POST, 'http://example.com/666'); + $this->mockRequest('POST', 'http://example.com/666'); $router = $this->router() ->get('/', function () { @@ -153,7 +152,7 @@ public function test_duplicate_routes_with_different_controllers() */ public function test_multiple_http_methods() { - $this->mockRequest(HttpMethods::POST, 'http://example.com/'); + $this->mockRequest('POST', 'http://example.com/'); $router = $this->router() ->get('/', function () { @@ -175,7 +174,7 @@ public function test_multiple_http_methods() */ public function test_with_a_required_parameter() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/666'); + $this->mockRequest('GET', 'http://web.com/666'); $router = $this->router() ->get('/{id}', function ($id) { @@ -191,7 +190,7 @@ public function test_with_a_required_parameter() */ public function test_with_a_optional_parameter_when_it_is_present() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/666'); + $this->mockRequest('GET', 'http://web.com/666'); $router = $this->router() ->get('/{id?}', function ($id) { @@ -207,7 +206,7 @@ public function test_with_a_optional_parameter_when_it_is_present() */ public function test_with_a_optional_parameter_when_it_is_not_present() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/'); + $this->mockRequest('GET', 'http://web.com/'); $router = $this->router() ->get('/{id?}', function ($id = 'Default') { @@ -223,21 +222,21 @@ public function test_with_a_optional_parameter_when_it_is_not_present() */ public function test_with_defined_parameters() { - $this->mockRequest(HttpMethods::GET, 'http://example.com/666'); + $this->mockRequest('GET', 'http://example.com/666'); $router = $this->router() - ->define('id', '[0-9]+') + ->patterns('id', '[0-9]+') ->get('/{id}', $this->OkController()) ->dispatch(); $this->assertEquals('OK', $this->output($router)); - $this->mockRequest(HttpMethods::GET, 'http://example.com/abc'); + $this->mockRequest('GET', 'http://example.com/abc'); $this->expectException(RouteNotFoundException::class); $this->router() - ->define('id', '[0-9]+') + ->patterns('id', '[0-9]+') ->get('/{id}', $this->OkController()) ->dispatch(); } @@ -299,23 +298,6 @@ public function test_injection_of_default_value() $this->assertEquals('Default', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_set_and_get_request() - { - $router = $this->router(); - - $router->get('/', function () use ($router) { - $newRequest = $router->getRequest()->withMethod('CUSTOM'); - $router->setRequest($newRequest); - - return $router->getRequest()->getMethod(); - })->dispatch(); - - $this->assertEquals('CUSTOM', $this->output($router)); - } - /** * @throws Throwable */ @@ -351,7 +333,7 @@ public function test_with_fully_namespaced_controller() */ public function test_not_found_error() { - $this->mockRequest(HttpMethods::GET, 'http://example.com/unknowon'); + $this->mockRequest('GET', 'http://example.com/unknowon'); $this->expectException(RouteNotFoundException::class); diff --git a/tests/TestCase.php b/tests/TestCase.php index 12cae5e..2eff41c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,11 +3,10 @@ namespace MiladRahimi\PhpRouter\Tests; use Closure; -use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; -use MiladRahimi\PhpRouter\Config; +use MiladRahimi\PhpRouter\Attributes; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -19,7 +18,7 @@ protected function setUp() { parent::setUp(); - $this->mockRequest(HttpMethods::GET, 'http://example.com/'); + $this->mockRequest('GET', 'http://example.com/'); } /** @@ -40,13 +39,13 @@ protected function mockRequest(string $method, string $url): void /** * Get a router instance for testing purposes * - * @param Config|null $config + * @param Attributes|null $config * @return Router */ - protected function router(Config $config = null): Router + protected function router(Attributes $config = null): Router { $router = new Router($config); - $router->setPublisher(new FakePublisher()); + $router->getContainer()->singleton(Publisher::class, new FakePublisher()); return $router; } @@ -71,7 +70,7 @@ protected function OkController(): Closure */ protected function output(Router $router) { - return $router->getPublisher()->output; + return $router->getContainer()->get(Publisher::class)->output; } /** @@ -82,7 +81,7 @@ protected function output(Router $router) */ protected function publisher(Router $router): FakePublisher { - return $router->getPublisher(); + return $router->getContainer()->get(Publisher::class); } /** diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 8a16e2b..ed10d92 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -2,7 +2,6 @@ namespace MiladRahimi\PhpRouter\Tests; -use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; use MiladRahimi\PhpRouter\Router; use Throwable; @@ -28,7 +27,7 @@ public function test_generating_url_for_the_homepage() */ public function test_generating_url_for_a_page() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/page'); + $this->mockRequest('GET', 'http://web.com/page'); $router = $this->router() ->get('/', function (Router $r) { @@ -47,7 +46,7 @@ public function test_generating_url_for_a_page() */ public function test_generating_url_for_a_page_with_required_parameter() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/contact'); + $this->mockRequest('GET', 'http://web.com/contact'); $router = $this->router() ->get('/{name}', function (Router $r) { @@ -63,7 +62,7 @@ public function test_generating_url_for_a_page_with_required_parameter() */ public function test_generating_url_for_a_page_with_optional_parameter() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/contact'); + $this->mockRequest('GET', 'http://web.com/contact'); $router = $this->router() ->get('/{name?}', function (Router $r) { @@ -79,7 +78,7 @@ public function test_generating_url_for_a_page_with_optional_parameter() */ public function test_generating_url_for_a_page_with_optional_parameter_2() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/contact'); + $this->mockRequest('GET', 'http://web.com/contact'); $router = $this->router() ->get('/{name?}', function (Router $r) { @@ -95,7 +94,7 @@ public function test_generating_url_for_a_page_with_optional_parameter_2() */ public function test_generating_url_for_a_page_with_optional_parameter_3() { - $this->mockRequest(HttpMethods::GET, 'http://web.com/page/contact'); + $this->mockRequest('GET', 'http://web.com/page/contact'); $router = $this->router() ->get('/page/?{name?}', function (Router $r) { From 17e719d062fbec096fc8b706d5424c53b43f53dd Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Thu, 26 Nov 2020 13:29:22 +0330 Subject: [PATCH 27/52] v5 --- README.md | 2 +- examples/example-10/index.php | 2 +- src/Attributes.php | 2 - src/Collection.php | 120 ---------------- src/Router.php | 118 ++++++++-------- src/{ => Routes}/Route.php | 14 +- src/Routes/RouteManager.php | 101 ++++++++++++++ src/Routes/RouteRepository.php | 73 ++++++++++ tests/ContainerTest.php | 4 +- tests/GroupingTest.php | 76 ++++++----- tests/MiddlewareTest.php | 24 ++-- tests/NamingTest.php | 12 +- tests/ResponseTest.php | 50 +++---- tests/RoutingTest.php | 242 +++++++++++++++++---------------- tests/UrlTest.php | 76 +++++------ 15 files changed, 490 insertions(+), 426 deletions(-) delete mode 100644 src/Collection.php rename src/{ => Routes}/Route.php (98%) create mode 100644 src/Routes/RouteManager.php create mode 100644 src/Routes/RouteRepository.php diff --git a/README.md b/README.md index 8a420f6..9d89a17 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ use MiladRahimi\PhpRouter\Router; $router = new Router(); -$router->patterns('id', '[0-9]+'); +$router->pattern('id', '[0-9]+'); $router->get('/blog/post/{id}', function (int $id) { return 'Content of the post: ' . $id; diff --git a/examples/example-10/index.php b/examples/example-10/index.php index c238395..6633264 100644 --- a/examples/example-10/index.php +++ b/examples/example-10/index.php @@ -6,7 +6,7 @@ $router = new Router(); -$router->patterns('id', '[0-9]+'); +$router->pattern('id', '[0-9]+'); $router->get('/post/{id}', function (int $id) { return 'Content of the post: ' . $id; diff --git a/src/Attributes.php b/src/Attributes.php index a100c87..d9d11da 100644 --- a/src/Attributes.php +++ b/src/Attributes.php @@ -10,8 +10,6 @@ class Attributes { const MIDDLEWARE = 'middleware'; - const PREFIX = 'prefix'; - const DOMAIN = 'domain'; } diff --git a/src/Collection.php b/src/Collection.php deleted file mode 100644 index 5c2d911..0000000 --- a/src/Collection.php +++ /dev/null @@ -1,120 +0,0 @@ - '', - Attributes::MIDDLEWARE => [], - Attributes::DOMAIN => null, - ]; - - /** - * Add a route to the collection - * - * @param string $method - * @param string $path - * @param $controller - * @param string|null $name - */ - public function add(string $method, string $path, $controller, ?string $name = null): void - { - $path = $this->attributes[Attributes::PREFIX] . $path; - $route = new Route( - $name, - $path, - $method, - $controller, - $this->attributes[Attributes::MIDDLEWARE], - $this->attributes[Attributes::DOMAIN] - ); - - $this->routes[$method][] = $route; - - $name && $this->names[$name] = $route; - } - - /** - * Find routes by given method - * - * @param string $method - * @return Route[] - */ - public function findByMethod(string $method): array - { - $routes = array_merge($this->routes['*'] ?? [], $this->routes[$method] ?? []); - - sort($routes, SORT_DESC); - - return $routes; - } - - /** - * Find route by given name - * - * @param string $name - * @return Route|null - */ - public function findByName(string $name): ?Route - { - return $this->names[$name] ?? null; - } - - /** - * @return Route[] - */ - public function getRoutes(): array - { - return $this->routes; - } - - /** - * @return Route[] - */ - public function getNames(): array - { - return $this->names; - } - - public function appendAttributes(array $newAttributes): void - { - $this->attributes[Attributes::PREFIX] .= $newAttributes[Attributes::PREFIX] ?? ''; - $this->attributes[Attributes::DOMAIN] = $newAttributes[Attributes::DOMAIN] ?? null; - $this->attributes[Attributes::MIDDLEWARE] = array_merge( - $this->attributes[Attributes::MIDDLEWARE], - $newAttributes[Attributes::MIDDLEWARE] ?? [] - ); - } - - /** - * @return array - */ - public function getAttributes(): array - { - return $this->attributes; - } - - /** - * @param array $attributes - */ - public function setAttributes(array $attributes): void - { - $this->attributes = $attributes; - } -} diff --git a/src/Router.php b/src/Router.php index 983e833..9d60653 100644 --- a/src/Router.php +++ b/src/Router.php @@ -9,6 +9,9 @@ use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; +use MiladRahimi\PhpRouter\Routes\Route; +use MiladRahimi\PhpRouter\Routes\RouteManager; +use MiladRahimi\PhpRouter\Routes\RouteRepository; use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use Psr\Container\ContainerInterface; @@ -25,12 +28,12 @@ class Router { /** - * @var Collection + * @var RouteManager */ - private $collection; + private $routeManager; /** - * List of defined route parameters + * List of defined route parameter patterns * * @var string[] */ @@ -48,13 +51,8 @@ class Router */ public function __construct() { - $this->collection = new Collection(); - $this->container = new Container(); - - $this->bind([Router::class], $this); - $this->bind([Container::class, ContainerInterface::class], $this->container); - $this->bind([Publisher::class], HttpPublisher::class); - $this->bind([ServerRequestInterface::class, ServerRequest::class], ServerRequestFactory::fromGlobals()); + $this->routeManager = new RouteManager(new RouteRepository()); + $this->setupContainer(); } /** @@ -62,19 +60,16 @@ public function __construct() * * @param array $attributes * @param Closure $body - * @return self */ - public function group(array $attributes, Closure $body): self + public function group(array $attributes, Closure $body): void { - $oldAttributes = $this->collection->getAttributes(); - $this->collection->appendAttributes($attributes); + $oldAttributes = $this->routeManager->getAttributes(); - call_user_func($body, $this); + $this->routeManager->appendAttributes($attributes); - // Revert to the old config - $this->collection->setAttributes($oldAttributes); + call_user_func($body, $this); - return $this; + $this->routeManager->setAttributes($oldAttributes); } /** @@ -84,35 +79,31 @@ public function group(array $attributes, Closure $body): self * @param string $path * @param Closure|array $controller * @param string|null $name - * @return self */ - public function map(string $method, string $path, $controller, ?string $name = null): self + public function map(string $method, string $path, $controller, ?string $name = null): void { - $this->collection->add($method, $path, $controller, $name); - - return $this; + $this->routeManager->add($method, $path, $controller, $name); } /** * Dispatch routes and run the application * - * @return self * @throws ContainerException * @throws InvalidCallableException * @throws NotFoundException * @throws RouteNotFoundException */ - public function dispatch(): self + public function dispatch() { /** @var ServerRequestInterface $request */ $request = $this->container->get(ServerRequestInterface::class); - foreach ($this->collection->findByMethod($request->getMethod()) as $route) { + $routes = $this->routeManager->getRepository()->findByMethod($request->getMethod()); + foreach ($routes as $route) { $parameters = []; if ($this->compare($route, $request, $parameters)) { $this->run($route, $parameters, $request); - - return $this; + return; } } @@ -134,10 +125,10 @@ private function run(Route $route, array $parameters, ServerRequestInterface $re $route->setParameters($this->pruneRouteParameters($parameters)); $route->setUri($request->getUri()->getPath()); - $this->bind([Route::class], $route); + $this->container->singleton(Route::class, $route); foreach ($parameters as $key => $value) { - $this->bind(['$' . $key], $value); + $this->container->singleton('$' . $key, $value); } /** @var Publisher $publisher */ @@ -175,7 +166,6 @@ private function pruneRouteParameters(array $parameters): array */ private function callStack(array $callables, ServerRequestInterface $request, $i = 0) { - $this->container->singleton('$request', $request); $this->container->singleton(ServerRequest::class, $request); $this->container->singleton(ServerRequestInterface::class, $request); @@ -325,7 +315,7 @@ private function regexParameter(string $name): string * @param string $pattern * @return self */ - public function patterns(string $name, string $pattern): self + public function pattern(string $name, string $pattern): self { $this->patterns[$name] = $pattern; @@ -342,7 +332,7 @@ public function patterns(string $name, string $pattern): self */ public function url(string $routeName, array $parameters = []): string { - if (!($route = $this->collection->findByName($routeName))) { + if (!($route = $this->routeManager->getRepository()->findByName($routeName))) { throw new UndefinedRouteException("There is no route with name `$routeName`."); } @@ -359,14 +349,22 @@ public function url(string $routeName, array $parameters = []): string } /** - * Bind dependencies using the container - * - * @param array $abstracts - * @param $concrete + * Setup IoC container */ - private function bind(array $abstracts, $concrete): void + private function setupContainer(): void { - foreach ($abstracts as $abstract) $this->container->singleton($abstract, $concrete); + $this->container = new Container(); + + $this->container->singleton(Router::class, $this); + + $this->container->singleton(Container::class, $this->container); + $this->container->singleton(ContainerInterface::class, $this->container); + + $this->container->singleton(Publisher::class, HttpPublisher::class); + + $request = ServerRequestFactory::fromGlobals(); + $this->container->singleton(ServerRequestInterface::class, $request); + $this->container->singleton(ServerRequest::class, $request); } /** @@ -376,15 +374,12 @@ private function bind(array $abstracts, $concrete): void * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function match(array $methods, string $path, $controller, ?string $name = null): self + public function match(array $methods, string $path, $controller, ?string $name = null): void { foreach ($methods as $method) { - return $this->map($method, $path, $controller, $name); + $this->map($method, $path, $controller, $name); } - - return $this; } /** @@ -393,11 +388,10 @@ public function match(array $methods, string $path, $controller, ?string $name = * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function any(string $path, $controller, ?string $name = null): self + public function any(string $path, $controller, ?string $name = null): void { - return $this->map('*', $path, $controller, $name); + $this->map('*', $path, $controller, $name); } /** @@ -406,11 +400,10 @@ public function any(string $path, $controller, ?string $name = null): self * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function get(string $path, $controller, ?string $name = null): self + public function get(string $path, $controller, ?string $name = null): void { - return $this->map('GET', $path, $controller, $name); + $this->map('GET', $path, $controller, $name); } /** @@ -419,11 +412,10 @@ public function get(string $path, $controller, ?string $name = null): self * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function post(string $path, $controller, ?string $name = null): self + public function post(string $path, $controller, ?string $name = null): void { - return $this->map('POST', $path, $controller, $name); + $this->map('POST', $path, $controller, $name); } /** @@ -432,11 +424,10 @@ public function post(string $path, $controller, ?string $name = null): self * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function put(string $path, $controller, ?string $name = null): self + public function put(string $path, $controller, ?string $name = null): void { - return $this->map('PUT', $path, $controller, $name); + $this->map('PUT', $path, $controller, $name); } /** @@ -445,11 +436,10 @@ public function put(string $path, $controller, ?string $name = null): self * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function patch(string $path, $controller, ?string $name = null): self + public function patch(string $path, $controller, ?string $name = null): void { - return $this->map('PATCH', $path, $controller, $name); + $this->map('PATCH', $path, $controller, $name); } /** @@ -458,11 +448,10 @@ public function patch(string $path, $controller, ?string $name = null): self * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function delete(string $path, $controller, ?string $name = null): self + public function delete(string $path, $controller, ?string $name = null): void { - return $this->map('DELETE', $path, $controller, $name); + $this->map('DELETE', $path, $controller, $name); } /** @@ -471,11 +460,10 @@ public function delete(string $path, $controller, ?string $name = null): self * @param string $path * @param Closure|callable|string $controller * @param string|null $name - * @return self */ - public function options(string $path, $controller, ?string $name = null): self + public function options(string $path, $controller, ?string $name = null): void { - return $this->map('OPTIONS', $path, $controller, $name); + $this->map('OPTIONS', $path, $controller, $name); } /** diff --git a/src/Route.php b/src/Routes/Route.php similarity index 98% rename from src/Route.php rename to src/Routes/Route.php index 3dc88f2..64bd8d7 100644 --- a/src/Route.php +++ b/src/Routes/Route.php @@ -1,6 +1,6 @@ name = $name; - $this->path = $path; $this->method = $method; + $this->path = $path; $this->controller = $controller; + $this->name = $name; $this->middleware = $middleware; $this->domain = $domain; } @@ -84,10 +84,10 @@ public function __construct( public function toArray(): array { return [ - 'name' => $this->name, - 'path' => $this->path, 'method' => $this->method, + 'path' => $this->path, 'controller' => $this->controller, + 'name' => $this->name, 'middleware' => $this->middleware, 'domain' => $this->domain, 'uri' => $this->uri, diff --git a/src/Routes/RouteManager.php b/src/Routes/RouteManager.php new file mode 100644 index 0000000..abb2bf3 --- /dev/null +++ b/src/Routes/RouteManager.php @@ -0,0 +1,101 @@ + '', + Attributes::MIDDLEWARE => [], + Attributes::DOMAIN => null, + ]; + + /** + * RouteManager constructor. + * + * @param RouteRepository $repository + */ + public function __construct(RouteRepository $repository) + { + $this->repository = $repository; + } + + /** + * Add a route to the collection + * + * @param string $method + * @param string $path + * @param $controller + * @param string|null $name + */ + public function add(string $method, string $path, $controller, ?string $name = null): void + { + $this->repository->save( + $method, + $this->attributes[Attributes::PREFIX] . $path, + $controller, + $name, + $this->attributes[Attributes::MIDDLEWARE], + $this->attributes[Attributes::DOMAIN] + ); + } + + /** + * Append new attributes to the existing ones + * + * @param array $attributes + */ + public function appendAttributes(array $attributes): void + { + $this->attributes[Attributes::DOMAIN] = $attributes[Attributes::DOMAIN] ?? null; + $this->attributes[Attributes::PREFIX] .= $attributes[Attributes::PREFIX] ?? ''; + $this->attributes[Attributes::MIDDLEWARE] = array_merge( + $this->attributes[Attributes::MIDDLEWARE], + $attributes[Attributes::MIDDLEWARE] ?? [] + ); + } + + /** + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @param array $attributes + */ + public function setAttributes(array $attributes): void + { + $this->attributes = $attributes; + } + + /** + * @return RouteRepository + */ + public function getRepository(): RouteRepository + { + return $this->repository; + } + + /** + * @param RouteRepository $repository + */ + public function setRepository(RouteRepository $repository): void + { + $this->repository = $repository; + } +} diff --git a/src/Routes/RouteRepository.php b/src/Routes/RouteRepository.php new file mode 100644 index 0000000..93a5f1f --- /dev/null +++ b/src/Routes/RouteRepository.php @@ -0,0 +1,73 @@ +repository['method'][$method][] = $route; + + if ($name) { + $this->repository['name'][$name] = $route; + } + } + + /** + * Find routes by given method + * + * @param string $method + * @return Route[] + */ + public function findByMethod(string $method): array + { + $routes = array_merge( + $this->repository['method']['*'] ?? [], + $this->repository['method'][$method] ?? [] + ); + + sort($routes, SORT_DESC); + + return $routes; + } + + /** + * Find route by given name + * + * @param string $name + * @return Route|null + */ + public function findByName(string $name): ?Route + { + return $this->repository['name'][$name] ?? null; + } +} diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 83df3aa..6eef73e 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -21,7 +21,9 @@ public function test_setting_and_getting_container() $router->get('/', function (Container $container) { return $container->get('name'); - })->dispatch(); + }); + + $router->dispatch(); $this->assertEquals('Pink Floyd', $this->output($router)); } diff --git a/tests/GroupingTest.php b/tests/GroupingTest.php index 9c9490e..0b0be1a 100644 --- a/tests/GroupingTest.php +++ b/tests/GroupingTest.php @@ -13,10 +13,11 @@ class GroupingTest extends TestCase */ public function test_with_no_attribute() { - $router = $this->router() - ->group([], function (Router $router) { - $router->get('/', $this->OkController()); - })->dispatch(); + $router = $this->router(); + $router->group([], function (Router $router) { + $router->get('/', $this->OkController()); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -28,10 +29,11 @@ public function test_with_a_middleware() { $middleware = new SampleMiddleware(666); - $router = $this->router() - ->group(['middleware' => [$middleware]], function (Router $router) { - $router->get('/', $this->OkController()); - })->dispatch(); + $router = $this->router(); + $router->group(['middleware' => [$middleware]], function (Router $router) { + $router->get('/', $this->OkController()); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); $this->assertContains($middleware->content, SampleMiddleware::$output); @@ -45,12 +47,13 @@ public function test_nested_groups_with_middleware() $group1Middleware = new SampleMiddleware(mt_rand(1, 9999999)); $group2Middleware = new SampleMiddleware(mt_rand(1, 9999999)); - $router = $this->router() - ->group(['middleware' => [$group1Middleware]], function (Router $router) use ($group2Middleware) { - $router->group(['middleware' => [$group2Middleware]], function (Router $router) { - $router->get('/', $this->OkController()); - }); - })->dispatch(); + $router = $this->router(); + $router->group(['middleware' => [$group1Middleware]], function (Router $router) use ($group2Middleware) { + $router->group(['middleware' => [$group2Middleware]], function (Router $router) { + $router->get('/', $this->OkController()); + }); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); $this->assertContains($group1Middleware->content, SampleMiddleware::$output); @@ -64,10 +67,11 @@ public function test_with_a_prefix() { $this->mockRequest('GET', 'http://example.com/group/page'); - $router = $this->router() - ->group(['prefix' => '/group'], function (Router $router) { - $router->get('/page', $this->OkController()); - })->dispatch(); + $router = $this->router(); + $router->group(['prefix' => '/group'], function (Router $router) { + $router->get('/page', $this->OkController()); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -79,12 +83,13 @@ public function test_nested_groups_with_prefix() { $this->mockRequest('GET', 'http://example.com/group1/group2/page'); - $router = $this->router() - ->group(['prefix' => '/group1'], function (Router $router) { - $router->group(['prefix' => '/group2'], function (Router $router) { - $router->get('/page', $this->OkController()); - }); - })->dispatch(); + $router = $this->router(); + $router->group(['prefix' => '/group1'], function (Router $router) { + $router->group(['prefix' => '/group2'], function (Router $router) { + $router->get('/page', $this->OkController()); + }); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -96,10 +101,11 @@ public function test_with_domain() { $this->mockRequest('GET', 'http://sub.domain.tld/'); - $router = $this->router() - ->group(['domain' => 'sub.domain.tld'], function (Router $router) { - $router->get('/', $this->OkController()); - })->dispatch(); + $router = $this->router(); + $router->group(['domain' => 'sub.domain.tld'], function (Router $router) { + $router->get('/', $this->OkController()); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -111,13 +117,13 @@ public function test_nested_groups_with_domain_it_should_consider_the_inner_grou { $this->mockRequest('GET', 'http://sub2.domain.com/'); - $router = $this->router() - ->group(['domain' => 'sub1.domain.com'], function (Router $router) { - $router->group(['domain' => 'sub2.domain.com'], function (Router $router) { - $router->get('/', $this->OkController()); - - }); - })->dispatch(); + $router = $this->router(); + $router->group(['domain' => 'sub1.domain.com'], function (Router $router) { + $router->group(['domain' => 'sub2.domain.com'], function (Router $router) { + $router->get('/', $this->OkController()); + }); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index b321004..c416628 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -17,9 +17,11 @@ public function test_with_a_single_middleware_as_an_object() { $middleware = new SampleMiddleware(666); - $router = $this->router()->group(['middleware' => [$middleware]], function (Router $r) { + $router = $this->router(); + $router->group(['middleware' => [$middleware]], function (Router $r) { $r->get('/', $this->OkController()); - })->dispatch(); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); $this->assertContains($middleware->content, SampleMiddleware::$output); @@ -32,9 +34,11 @@ public function test_with_a_single_middleware_as_a_string() { $middleware = SampleMiddleware::class; - $router = $this->router()->group(['middleware' => [$middleware]], function (Router $r) { + $router = $this->router(); + $router->group(['middleware' => [$middleware]], function (Router $r) { $r->get('/', $this->OkController()); - })->dispatch(); + }); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); $this->assertEquals('empty', SampleMiddleware::$output[0]); @@ -47,9 +51,11 @@ public function test_with_a_stopper_middleware() { $middleware = new StopperMiddleware(666); - $router = $this->router()->group(['middleware' => [$middleware]], function (Router $r) { + $router = $this->router(); + $router->group(['middleware' => [$middleware]], function (Router $r) { $r->get('/', $this->OkController()); - })->dispatch(); + }); + $router->dispatch(); $this->assertEquals('Stopped in middleware.', $this->output($router)); $this->assertContains($middleware->content, StopperMiddleware::$output); @@ -62,8 +68,10 @@ public function test_with_invalid_middleware() { $this->expectException(InvalidCallableException::class); - $this->router()->group(['middleware' => ['UnknownMiddleware']], function (Router $r) { + $router = $this->router(); + $router->group(['middleware' => ['UnknownMiddleware']], function (Router $r) { $r->get('/', $this->OkController()); - })->dispatch(); + }); + $router->dispatch(); } } diff --git a/tests/NamingTest.php b/tests/NamingTest.php index e09e6d1..b50cba9 100644 --- a/tests/NamingTest.php +++ b/tests/NamingTest.php @@ -3,7 +3,7 @@ namespace MiladRahimi\PhpRouter\Tests; use MiladRahimi\PhpRouter\Enums\HttpMethods; -use MiladRahimi\PhpRouter\Route; +use MiladRahimi\PhpRouter\Routes\Route; use Throwable; class NamingTest extends TestCase @@ -13,11 +13,11 @@ class NamingTest extends TestCase */ public function test_a_named_route() { - $router = $this->router() - ->get('/', function (Route $route) { - return $route->getName(); - }, 'home') - ->dispatch(); + $router = $this->router(); + $router->get('/', function (Route $route) { + return $route->getName(); + }, 'home'); + $router->dispatch(); $this->assertEquals('home', $this->output($router)); } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index e646ed0..1cb1912 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -16,11 +16,11 @@ class ResponseTest extends TestCase */ public function test_empty_response_with_code_204() { - $router = $this->router() - ->get('/', function () { - return new EmptyResponse(204); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return new EmptyResponse(204); + }); + $router->dispatch(); $this->assertEquals(204, $this->status($router)); } @@ -30,11 +30,11 @@ public function test_empty_response_with_code_204() */ public function test_html_response_with_code_200() { - $router = $this->router() - ->get('/', function () { - return new HtmlResponse('', 200); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return new HtmlResponse('', 200); + }); + $router->dispatch(); $this->assertEquals(200, $this->status($router)); $this->assertEquals('', $this->output($router)); @@ -45,11 +45,11 @@ public function test_html_response_with_code_200() */ public function test_json_response_with_code_201() { - $router = $this->router() - ->get('/', function () { - return new JsonResponse(['a' => 'x', 'b' => 'y'], 201); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return new JsonResponse(['a' => 'x', 'b' => 'y'], 201); + }); + $router->dispatch(); $this->assertEquals(201, $this->status($router)); $this->assertEquals(json_encode(['a' => 'x', 'b' => 'y']), $this->output($router)); @@ -60,11 +60,11 @@ public function test_json_response_with_code_201() */ public function test_text_response_with_code_203() { - $router = $this->router() - ->get('/', function () { - return new TextResponse('Content', 203); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return new TextResponse('Content', 203); + }); + $router->dispatch(); $this->assertEquals(203, $this->status($router)); $this->assertEquals('Content', $this->output($router)); @@ -75,11 +75,11 @@ public function test_text_response_with_code_203() */ public function test_redirect_response_with_code_203() { - $router = $this->router() - ->get('/', function () { - return new RedirectResponse('https://miladrahimi.com'); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return new RedirectResponse('https://miladrahimi.com'); + }); + $router->dispatch(); $this->assertEquals(302, $this->status($router)); $this->assertEquals('', $this->output($router)); diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 5910b8f..6e3b90c 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -6,7 +6,7 @@ use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Tests\Testing\SampleController; -use MiladRahimi\PhpRouter\Route; +use MiladRahimi\PhpRouter\Routes\Route; use Psr\Http\Message\ServerRequestInterface; use Throwable; use Laminas\Diactoros\ServerRequest; @@ -20,7 +20,9 @@ public function test_a_simple_get_route() { $this->mockRequest('GET', 'http://example.com/'); - $router = $this->router()->map('GET', '/', $this->OkController())->dispatch(); + $router = $this->router(); + $router->map('GET', '/', $this->OkController()); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -32,7 +34,9 @@ public function test_a_simple_post_route() { $this->mockRequest('POST', 'http://example.com/'); - $router = $this->router()->post('/', $this->OkController())->dispatch(); + $router = $this->router(); + $router->post('/', $this->OkController()); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -44,7 +48,9 @@ public function test_a_simple_put_route() { $this->mockRequest('PUT', 'http://example.com/'); - $router = $this->router()->put('/', $this->OkController())->dispatch(); + $router = $this->router(); + $router->put('/', $this->OkController()); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -56,7 +62,9 @@ public function test_a_simple_patch_route() { $this->mockRequest('PATCH', 'http://example.com/'); - $router = $this->router()->patch('/', $this->OkController())->dispatch(); + $router = $this->router(); + $router->patch('/', $this->OkController()); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -68,7 +76,9 @@ public function test_a_simple_delete_route() { $this->mockRequest('DELETE', 'http://example.com/'); - $router = $this->router()->delete('/', $this->OkController())->dispatch(); + $router = $this->router(); + $router->delete('/', $this->OkController()); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -82,7 +92,9 @@ public function test_a_route_with_custom_method() $this->mockRequest($method, 'http://example.com/'); - $router = $this->router()->map($method, '/', $this->OkController())->dispatch(); + $router = $this->router(); + $router->map($method, '/', $this->OkController()); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); } @@ -94,19 +106,21 @@ public function test_the_any_method() { $this->mockRequest('GET', 'http://example.com/'); - $router = $this->router()->any('/', function () { + $router = $this->router(); + $router->any('/', function () { return 'Test any for get'; - })->dispatch(); + }); + $router->dispatch(); $this->assertEquals('Test any for get', $this->output($router)); $this->mockRequest('POST', 'http://example.com/'); - $router = $this->router() - ->any('/', function () { - return 'Test any for post'; - }) - ->dispatch(); + $router = $this->router(); + $router->any('/', function () { + return 'Test any for post'; + }); + $router->dispatch(); $this->assertEquals('Test any for post', $this->output($router)); } @@ -118,14 +132,14 @@ public function test_multiple_routes() { $this->mockRequest('POST', 'http://example.com/666'); - $router = $this->router() - ->get('/', function () { - return 'Home'; - }) - ->post('/{id}', function ($id) { - return $id; - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return 'Home'; + }); + $router->post('/{id}', function ($id) { + return $id; + }); + $router->dispatch(); $this->assertEquals('666', $this->output($router)); } @@ -135,14 +149,14 @@ public function test_multiple_routes() */ public function test_duplicate_routes_with_different_controllers() { - $router = $this->router() - ->get('/', function () { - return 'Home'; - }) - ->get('/', function () { - return 'Home again!'; - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return 'Home'; + }); + $router->get('/', function () { + return 'Home again!'; + }); + $router->dispatch(); $this->assertEquals('Home again!', $this->output($router)); } @@ -154,17 +168,17 @@ public function test_multiple_http_methods() { $this->mockRequest('POST', 'http://example.com/'); - $router = $this->router() - ->get('/', function () { - return 'Get'; - }) - ->post('/', function () { - return 'Post'; - }) - ->delete('/', function () { - return 'Delete'; - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function () { + return 'Get'; + }); + $router->post('/', function () { + return 'Post'; + }); + $router->delete('/', function () { + return 'Delete'; + }); + $router->dispatch(); $this->assertEquals('Post', $this->output($router)); } @@ -176,11 +190,11 @@ public function test_with_a_required_parameter() { $this->mockRequest('GET', 'http://web.com/666'); - $router = $this->router() - ->get('/{id}', function ($id) { - return $id; - }) - ->dispatch(); + $router = $this->router(); + $router->get('/{id}', function ($id) { + return $id; + }); + $router->dispatch(); $this->assertEquals('666', $this->output($router)); } @@ -192,11 +206,11 @@ public function test_with_a_optional_parameter_when_it_is_present() { $this->mockRequest('GET', 'http://web.com/666'); - $router = $this->router() - ->get('/{id?}', function ($id) { - return $id; - }) - ->dispatch(); + $router = $this->router(); + $router->get('/{id?}', function ($id) { + return $id; + }); + $router->dispatch(); $this->assertEquals('666', $this->output($router)); } @@ -208,11 +222,11 @@ public function test_with_a_optional_parameter_when_it_is_not_present() { $this->mockRequest('GET', 'http://web.com/'); - $router = $this->router() - ->get('/{id?}', function ($id = 'Default') { - return $id; - }) - ->dispatch(); + $router = $this->router(); + $router->get('/{id?}', function ($id = 'Default') { + return $id; + }); + $router->dispatch(); $this->assertEquals('Default', $this->output($router)); } @@ -224,10 +238,10 @@ public function test_with_defined_parameters() { $this->mockRequest('GET', 'http://example.com/666'); - $router = $this->router() - ->patterns('id', '[0-9]+') - ->get('/{id}', $this->OkController()) - ->dispatch(); + $router = $this->router(); + $router->pattern('id', '[0-9]+'); + $router->get('/{id}', $this->OkController()); + $router->dispatch(); $this->assertEquals('OK', $this->output($router)); @@ -235,25 +249,10 @@ public function test_with_defined_parameters() $this->expectException(RouteNotFoundException::class); - $this->router() - ->patterns('id', '[0-9]+') - ->get('/{id}', $this->OkController()) - ->dispatch(); - } - - /** - * @throws Throwable - */ - public function test_injection_of_request_by_name() - { - $router = $this->router() - ->get('/', function ($request) { - /** @var ServerRequest $request */ - return $request->getMethod(); - }) - ->dispatch(); - - $this->assertEquals('GET', $this->output($router)); + $router = $this->router(); + $router->pattern('id', '[0-9]+'); + $router->get('/{id}', $this->OkController()); + $router->dispatch(); } /** @@ -261,11 +260,11 @@ public function test_injection_of_request_by_name() */ public function test_injection_of_request_by_interface() { - $router = $this->router() - ->get('/', function (ServerRequestInterface $r) { - return $r->getMethod(); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function (ServerRequestInterface $r) { + return $r->getMethod(); + }); + $router->dispatch(); $this->assertEquals('GET', $this->output($router)); } @@ -275,11 +274,11 @@ public function test_injection_of_request_by_interface() */ public function test_injection_of_request_by_type() { - $router = $this->router() - ->get('/', function (ServerRequest $r) { - return $r->getMethod(); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function (ServerRequest $r) { + return $r->getMethod(); + }); + $router->dispatch(); $this->assertEquals('GET', $this->output($router)); } @@ -289,11 +288,11 @@ public function test_injection_of_request_by_type() */ public function test_injection_of_default_value() { - $router = $this->router() - ->get('/', function ($default = "Default") { - return $default; - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function ($default = "Default") { + return $default; + }); + $router->dispatch(); $this->assertEquals('Default', $this->output($router)); } @@ -309,7 +308,8 @@ public function test_default_publisher() $router->get('/', function () { return 'home'; - })->dispatch(); + }); + $router->dispatch(); $this->assertEquals('home', ob_get_contents()); @@ -321,9 +321,9 @@ public function test_default_publisher() */ public function test_with_fully_namespaced_controller() { - $router = $this->router() - ->get('/', [SampleController::class, 'home']) - ->dispatch(); + $router = $this->router(); + $router->get('/', [SampleController::class, 'home']); + $router->dispatch(); $this->assertEquals('Home', $this->output($router)); } @@ -337,7 +337,9 @@ public function test_not_found_error() $this->expectException(RouteNotFoundException::class); - $this->router()->get('/', $this->OkController())->dispatch(); + $router = $this->router(); + $router->get('/', $this->OkController()); + $router->dispatch(); } /** @@ -347,7 +349,9 @@ public function test_with_class_method_but_invalid_controller_class() { $this->expectException(InvalidCallableException::class); - $this->router()->get('/', 'UnknownController@method')->dispatch(); + $router = $this->router(); + $router->get('/', 'UnknownController@method'); + $router->dispatch(); } /** @@ -357,7 +361,9 @@ public function test_with_class_but_invalid_method() { $this->expectException(InvalidCallableException::class); - $this->router()->get('/', SampleController::class . '@invalid')->dispatch(); + $router = $this->router(); + $router->get('/', SampleController::class . '@invalid'); + $router->dispatch(); } /** @@ -367,7 +373,9 @@ public function test_with_invalid_controller_class() { $this->expectException(InvalidCallableException::class); - $this->router()->get('/', 666)->dispatch(); + $router = $this->router(); + $router->get('/', 666); + $router->dispatch(); } /** @@ -375,21 +383,21 @@ public function test_with_invalid_controller_class() */ public function test_current_route() { - $router = $this->router() - ->get('/', function (Route $r) { - return join(',', [ - $r->getName(), - $r->getPath(), - $r->getUri(), - $r->getParameters(), - $r->getMethod(), - count($r->getMiddleware()), - $r->getDomain() ?? '-', - ]); - }, 'home') - ->dispatch(); - - $value = join(',', ['home', '/','/', [], 'GET', 0, '-']); + $router = $this->router(); + $router->get('/', function (Route $r) { + return join(',', [ + $r->getName(), + $r->getPath(), + $r->getUri(), + $r->getParameters(), + $r->getMethod(), + count($r->getMiddleware()), + $r->getDomain() ?? '-', + ]); + }, 'home'); + $router->dispatch(); + + $value = join(',', ['home', '/', '/', [], 'GET', 0, '-']); $this->assertEquals($value, $this->output($router)); } } diff --git a/tests/UrlTest.php b/tests/UrlTest.php index ed10d92..74dd6d0 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -13,11 +13,11 @@ class UrlTest extends TestCase */ public function test_generating_url_for_the_homepage() { - $router = $this->router() - ->get('/', function (Router $r) { - return $r->url('home'); - }, 'home') - ->dispatch(); + $router = $this->router(); + $router->get('/', function (Router $r) { + return $r->url('home'); + }, 'home'); + $router->dispatch(); $this->assertEquals('/', $this->output($router)); } @@ -29,14 +29,14 @@ public function test_generating_url_for_a_page() { $this->mockRequest('GET', 'http://web.com/page'); - $router = $this->router() - ->get('/', function (Router $r) { - return $r->url('home'); - }, 'home') - ->get('/page', function (Router $r) { - return $r->url('page'); - }, 'page') - ->dispatch(); + $router = $this->router(); + $router->get('/', function (Router $r) { + return $r->url('home'); + }, 'home'); + $router->get('/page', function (Router $r) { + return $r->url('page'); + }, 'page'); + $router->dispatch(); $this->assertEquals('/page', $this->output($router)); } @@ -48,11 +48,11 @@ public function test_generating_url_for_a_page_with_required_parameter() { $this->mockRequest('GET', 'http://web.com/contact'); - $router = $this->router() - ->get('/{name}', function (Router $r) { - return $r->url('page', ['name' => 'about']); - }, 'page') - ->dispatch(); + $router = $this->router(); + $router->get('/{name}', function (Router $r) { + return $r->url('page', ['name' => 'about']); + }, 'page'); + $router->dispatch(); $this->assertEquals('/about', $this->output($router)); } @@ -64,11 +64,11 @@ public function test_generating_url_for_a_page_with_optional_parameter() { $this->mockRequest('GET', 'http://web.com/contact'); - $router = $this->router() - ->get('/{name?}', function (Router $r) { - return $r->url('page', ['name' => 'about']); - }, 'page') - ->dispatch(); + $router = $this->router(); + $router->get('/{name?}', function (Router $r) { + return $r->url('page', ['name' => 'about']); + }, 'page'); + $router->dispatch(); $this->assertEquals('/about', $this->output($router)); } @@ -80,11 +80,11 @@ public function test_generating_url_for_a_page_with_optional_parameter_2() { $this->mockRequest('GET', 'http://web.com/contact'); - $router = $this->router() - ->get('/{name?}', function (Router $r) { - return $r->url('page'); - }, 'page') - ->dispatch(); + $router = $this->router(); + $router->get('/{name?}', function (Router $r) { + return $r->url('page'); + }, 'page'); + $router->dispatch(); $this->assertEquals('/', $this->output($router)); } @@ -96,11 +96,11 @@ public function test_generating_url_for_a_page_with_optional_parameter_3() { $this->mockRequest('GET', 'http://web.com/page/contact'); - $router = $this->router() - ->get('/page/?{name?}', function (Router $r) { - return $r->url('page'); - }, 'page') - ->dispatch(); + $router = $this->router(); + $router->get('/page/?{name?}', function (Router $r) { + return $r->url('page'); + }, 'page'); + $router->dispatch(); $this->assertEquals('/page', $this->output($router)); } @@ -113,10 +113,10 @@ public function test_generating_url_for_undefined_route() $this->expectException(UndefinedRouteException::class); $this->expectExceptionMessage("There is no route with name `home`."); - $this->router() - ->get('/', function (Router $r) { - return $r->url('home'); - }) - ->dispatch(); + $router = $this->router(); + $router->get('/', function (Router $r) { + return $r->url('home'); + }); + $router->dispatch(); } } From 6daf49141833168837e2e1999aada36c2d5a3ffe Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Fri, 27 Nov 2020 23:27:36 +0330 Subject: [PATCH 28/52] v5 --- src/Attributes.php | 5 - src/Dispatching/Caller.php | 109 +++++ src/Dispatching/Matcher.php | 142 ++++++ src/Router.php | 420 ++++++------------ src/Routes/Route.php | 5 - src/Routes/RouteManager.php | 101 ----- src/Routes/{RouteRepository.php => Store.php} | 7 +- src/Routes/Storekeeper.php | 102 +++++ src/Url.php | 50 +++ tests/ContainerTest.php | 3 +- tests/RoutingTest.php | 2 +- tests/TestCase.php | 14 +- tests/UrlTest.php | 35 +- 13 files changed, 573 insertions(+), 422 deletions(-) create mode 100644 src/Dispatching/Caller.php create mode 100644 src/Dispatching/Matcher.php delete mode 100644 src/Routes/RouteManager.php rename src/Routes/{RouteRepository.php => Store.php} (93%) create mode 100644 src/Routes/Storekeeper.php create mode 100644 src/Url.php diff --git a/src/Attributes.php b/src/Attributes.php index d9d11da..4cd009a 100644 --- a/src/Attributes.php +++ b/src/Attributes.php @@ -2,11 +2,6 @@ namespace MiladRahimi\PhpRouter; -/** - * Class Attributes - * - * @package MiladRahimi\PhpRouter - */ class Attributes { const MIDDLEWARE = 'middleware'; diff --git a/src/Dispatching/Caller.php b/src/Dispatching/Caller.php new file mode 100644 index 0000000..ec99c7f --- /dev/null +++ b/src/Dispatching/Caller.php @@ -0,0 +1,109 @@ +container = $container; + } + + /** + * Call the given callable stack + * + * @param string[] $callables + * @param ServerRequestInterface $request + * @param int $i + * @return mixed + * @throws ContainerException + * @throws InvalidCallableException + */ + public function stack(array $callables, ServerRequestInterface $request, $i = 0) + { + $this->container->singleton(ServerRequest::class, $request); + $this->container->singleton(ServerRequestInterface::class, $request); + + if (isset($callables[$i + 1])) { + $next = function (ServerRequestInterface $request) use ($callables, $i) { + return $this->stack($callables, $request, $i + 1); + }; + + $this->container->closure('$next', $next); + } else { + $this->container->delete('$next'); + } + + return $this->call($callables[$i]); + } + + /** + * Run the given callable + * + * @param Closure|callable|string $callable + * @return mixed + * @throws InvalidCallableException + * @throws ContainerException + */ + public function call($callable) + { + if (is_array($callable)) { + if (count($callable) != 2) { + throw new InvalidCallableException('Invalid callable: ' . implode(',', $callable)); + } + + list($class, $method) = $callable; + + if (class_exists($class) == false) { + throw new InvalidCallableException("Class `$callable` not found."); + } + + $object = new $class(); + + if (method_exists($object, $method) == false) { + throw new InvalidCallableException("Method `$class::$method` not found."); + } + + $callable = [$object, $method]; + } else { + if (is_string($callable)) { + if (class_exists($callable)) { + $callable = new $callable(); + } else { + throw new InvalidCallableException("Class `$callable` not found."); + } + } + + if (is_object($callable) && !($callable instanceof Closure)) { + if (method_exists($callable, 'handle')) { + $callable = [$callable, 'handle']; + } else { + throw new InvalidCallableException("Method `$callable::handle` not found."); + } + } + } + + if (is_callable($callable) == false) { + throw new InvalidCallableException('Invalid callable.'); + } + + return $this->container->call($callable); + } +} diff --git a/src/Dispatching/Matcher.php b/src/Dispatching/Matcher.php new file mode 100644 index 0000000..333e56a --- /dev/null +++ b/src/Dispatching/Matcher.php @@ -0,0 +1,142 @@ +store = $store; + } + + /** + * Find the right route for the given request and defined patterns + * + * @param ServerRequestInterface $request + * @param string[] $patterns + * @return Route + * @throws RouteNotFoundException + */ + public function find(ServerRequestInterface $request, array $patterns) + { + foreach ($this->store->findByMethod($request->getMethod()) as $route) { + $parameters = []; + + if ($this->compare($route, $request, $parameters, $patterns)) { + $route->setUri($request->getUri()->getPath()); + $route->setParameters($this->pruneRouteParameters($parameters)); + + return $route; + } + } + + throw new RouteNotFoundException(); + } + + /** + * Prune route parameters (remove unnecessary parameters) + * + * @param string[] $parameters + * @return string[] + * @noinspection PhpUnusedParameterInspection + */ + private function pruneRouteParameters(array $parameters): array + { + return array_filter($parameters, function ($value, $name) { + return is_numeric($name) == false; + }, ARRAY_FILTER_USE_BOTH); + } + + /** + * Compare given route with the given http request + * + * @param Route $route + * @param ServerRequestInterface $request + * @param string[] $parameters + * @param string[] $patterns + * @return bool + */ + private function compare(Route $route, ServerRequestInterface $request, array &$parameters, array $patterns): bool + { + return ( + $this->compareDomain($route->getDomain(), $request->getUri()->getHost()) && + $this->compareUri($route->getPath(), $request->getUri()->getPath(), $parameters, $patterns) + ); + } + + /** + * Check if given request domain matches given route domain + * + * @param string|null $routeDomain + * @param string $requestDomain + * @return bool + */ + private function compareDomain(?string $routeDomain, string $requestDomain): bool + { + return !$routeDomain || preg_match('@^' . $routeDomain . '$@', $requestDomain); + } + + /** + * Check if given request uri matches given uri method + * + * @param string $path + * @param string $uri + * @param string[] $parameters + * @param string[] $patterns + * @return bool + */ + private function compareUri(string $path, string $uri, array &$parameters, array $patterns): bool + { + return preg_match('@^' . $this->regexUri($path, $patterns) . '$@', $uri, $parameters); + } + + /** + * Convert route to regex + * + * @param string $path + * @param string[] $patterns + * @return string + */ + private function regexUri(string $path, array $patterns): string + { + return preg_replace_callback('@{([^}]+)}@', function (array $match) use ($patterns) { + return $this->regexParameter($match[1], $patterns); + }, $path); + } + + /** + * Convert route parameter to regex + * + * @param string $name + * @param array $patterns + * @return string + */ + private function regexParameter(string $name, array $patterns): string + { + if ($name[-1] == '?') { + $name = substr($name, 0, -1); + $suffix = '?'; + } else { + $suffix = ''; + } + + $pattern = $patterns[$name] ?? '[^/]+'; + + return '(?<' . $name . '>' . $pattern . ')' . $suffix; + } +} \ No newline at end of file diff --git a/src/Router.php b/src/Router.php index 9d60653..da9776e 100644 --- a/src/Router.php +++ b/src/Router.php @@ -5,40 +5,20 @@ use Closure; use MiladRahimi\PhpContainer\Container; use MiladRahimi\PhpContainer\Exceptions\ContainerException; -use MiladRahimi\PhpContainer\Exceptions\NotFoundException; +use MiladRahimi\PhpRouter\Dispatching\Caller; +use MiladRahimi\PhpRouter\Dispatching\Matcher; use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; -use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; use MiladRahimi\PhpRouter\Routes\Route; -use MiladRahimi\PhpRouter\Routes\RouteManager; -use MiladRahimi\PhpRouter\Routes\RouteRepository; +use MiladRahimi\PhpRouter\Routes\Storekeeper; +use MiladRahimi\PhpRouter\Routes\Store; use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use Psr\Container\ContainerInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; -/** - * Class Router - * - * @package MiladRahimi\PhpRouter - */ class Router { - /** - * @var RouteManager - */ - private $routeManager; - - /** - * List of defined route parameter patterns - * - * @var string[] - */ - private $patterns = []; - /** * The dependency injection IoC container * @@ -47,324 +27,144 @@ class Router private $container; /** - * Router constructor. + * The storekeeper of route store (repository) + * + * @var Storekeeper */ - public function __construct() - { - $this->routeManager = new RouteManager(new RouteRepository()); - $this->setupContainer(); - } + private $storekeeper; /** - * Group routes with the given attributes + * The route matcher that finds appropriate routes for requests * - * @param array $attributes - * @param Closure $body + * @var Matcher */ - public function group(array $attributes, Closure $body): void - { - $oldAttributes = $this->routeManager->getAttributes(); - - $this->routeManager->appendAttributes($attributes); - - call_user_func($body, $this); - - $this->routeManager->setAttributes($oldAttributes); - } + private $matcher; /** - * Map a controller to a route + * The callable caller that calls (invokes) middleware and controllers * - * @param string $method - * @param string $path - * @param Closure|array $controller - * @param string|null $name + * @var Caller */ - public function map(string $method, string $path, $controller, ?string $name = null): void - { - $this->routeManager->add($method, $path, $controller, $name); - } + private $caller; /** - * Dispatch routes and run the application + * The publisher that publish controller outputs * - * @throws ContainerException - * @throws InvalidCallableException - * @throws NotFoundException - * @throws RouteNotFoundException + * @var Publisher */ - public function dispatch() - { - /** @var ServerRequestInterface $request */ - $request = $this->container->get(ServerRequestInterface::class); - - $routes = $this->routeManager->getRepository()->findByMethod($request->getMethod()); - foreach ($routes as $route) { - $parameters = []; - if ($this->compare($route, $request, $parameters)) { - $this->run($route, $parameters, $request); - return; - } - } - - throw new RouteNotFoundException(); - } + private $publisher; /** - * Run the route controllers and related middleware + * List of defined parameter patterns * - * @param Route $route - * @param array $parameters - * @param ServerRequestInterface $request - * @throws ContainerException - * @throws InvalidCallableException - * @throws NotFoundException + * @var string[] */ - private function run(Route $route, array $parameters, ServerRequestInterface $request) - { - $route->setParameters($this->pruneRouteParameters($parameters)); - $route->setUri($request->getUri()->getPath()); - - $this->container->singleton(Route::class, $route); - - foreach ($parameters as $key => $value) { - $this->container->singleton('$' . $key, $value); - } - - /** @var Publisher $publisher */ - $publisher = $this->container->get(Publisher::class); - $publisher->publish($this->callStack( - array_merge($route->getMiddleware(), [$route->getController()]), - $request - )); - } + private $patterns = []; /** - * Prune route parameters (remove unnecessary parameters) + * Router constructor. * - * @param array $parameters - * @return array - * @noinspection PhpUnusedParameterInspection + * @param Container $container + * @param Storekeeper $storekeeper + * @param Matcher $matcher + * @param Caller $caller + * @param Publisher $publisher */ - private function pruneRouteParameters(array $parameters): array + public function __construct( + Container $container, + Storekeeper $storekeeper, + Matcher $matcher, + Caller $caller, + Publisher $publisher + ) { - return array_filter($parameters, function ($value, $name) { - return is_numeric($name) == false; - }, ARRAY_FILTER_USE_BOTH); + $this->container = $container; + $this->storekeeper = $storekeeper; + $this->matcher = $matcher; + $this->caller = $caller; + $this->publisher = $publisher; } /** - * Call the given callable stack + * Create a new router instance * - * @param string[] $callables - * @param ServerRequestInterface $request - * @param int $i - * @return ResponseInterface|mixed|null + * @return static * @throws ContainerException - * @throws InvalidCallableException - * @throws NotFoundException */ - private function callStack(array $callables, ServerRequestInterface $request, $i = 0) + public static function create(): self { - $this->container->singleton(ServerRequest::class, $request); - $this->container->singleton(ServerRequestInterface::class, $request); + $container = new Container(); + $container->singleton(Container::class, $container); + $container->singleton(ContainerInterface::class, $container); + $container->singleton(Store::class, new Store()); + $container->singleton(Publisher::class, HttpPublisher::class); - if (isset($callables[$i + 1])) { - $next = function (ServerRequestInterface $request) use ($callables, $i) { - return $this->callStack($callables, $request, $i + 1); - }; - - $this->container->closure('$next', $next); - } - - return $this->runCallable($callables[$i]); + return $container->instantiate(Router::class); } /** - * Run the given callable (controller or middleware) + * Group routes with the given attributes * - * @param Closure|callable|string $callable - * @return ResponseInterface|mixed|null - * @throws ContainerException - * @throws InvalidCallableException - * @throws NotFoundException + * @param array $attributes + * @param Closure $body */ - private function runCallable($callable) + public function group(array $attributes, Closure $body): void { - if (is_array($callable)) { - if (count($callable) != 2) { - throw new InvalidCallableException('Invalid callable: ' . implode(',', $callable)); - } - - list($class, $method) = $callable; - - if (class_exists($class) == false) { - throw new InvalidCallableException("Class `$callable` not found."); - } - - $object = new $class(); - - if (method_exists($object, $method) == false) { - throw new InvalidCallableException("Method `$class::$method` not found."); - } - - $callable = [$object, $method]; - } else { - if (is_string($callable)) { - if (class_exists($callable)) { - $callable = new $callable(); - } else { - throw new InvalidCallableException("Class `$callable` not found."); - } - } - - if (is_object($callable) && !($callable instanceof Closure)) { - if (method_exists($callable, 'handle')) { - $callable = [$callable, 'handle']; - } else { - throw new InvalidCallableException("Method `$callable::handle` not found."); - } - } - } + $oldAttributes = $this->storekeeper->attributes(); - if (is_callable($callable) == false) { - throw new InvalidCallableException('Invalid callable.'); - } + $this->storekeeper->appendAttributes($attributes); - return $this->container->call($callable); - } - - /** - * Compare given route with the given http request - * - * @param Route $route - * @param ServerRequestInterface $request - * @param array $parameters - * @return bool - */ - private function compare(Route $route, ServerRequestInterface $request, array &$parameters): bool - { - return ( - $this->compareDomain($route->getDomain(), $request->getUri()->getHost()) && - $this->compareUri($route->getPath(), $request->getUri()->getPath(), $parameters) - ); - } + call_user_func($body, $this); - /** - * Check if given request domain matches given route domain - * - * @param string|null $routeDomain - * @param string $requestDomain - * @return bool - */ - private function compareDomain(?string $routeDomain, string $requestDomain): bool - { - return !$routeDomain || preg_match('@^' . $routeDomain . '$@', $requestDomain); + $this->storekeeper->updateAttributes($oldAttributes); } /** - * Check if given request uri matches given uri method + * Map a controller to a route * + * @param string $method * @param string $path - * @param string $uri - * @param array $parameters - * @return bool + * @param Closure|array $controller + * @param string|null $name */ - private function compareUri(string $path, string $uri, array &$parameters): bool + public function map(string $method, string $path, $controller, ?string $name = null): void { - return preg_match('@^' . $this->regexUri($path) . '$@', $uri, $parameters); + $this->storekeeper->add($method, $path, $controller, $name); } /** - * Convert route to regex + * Dispatch routes and run the application * - * @param string $route - * @return string + * @throws InvalidCallableException + * @throws RouteNotFoundException */ - private function regexUri(string $route): string + public function dispatch() { - return preg_replace_callback('@{([^}]+)}@', function (array $match) { - return $this->regexParameter($match[1]); - }, $route); - } + $request = ServerRequestFactory::fromGlobals(); - /** - * Convert route parameter to regex - * - * @param string $name - * @return string - */ - private function regexParameter(string $name): string - { - if ($name[-1] == '?') { - $name = substr($name, 0, -1); - $suffix = '?'; - } else { - $suffix = ''; - } + $route = $this->matcher->find($request, $this->patterns); + + $this->container->singleton(Route::class, $route); - $pattern = $this->patterns[$name] ?? '[^/]+'; + foreach ($route->getParameters() as $key => $value) { + $this->container->singleton('$' . $key, $value); + } - return '(?<' . $name . '>' . $pattern . ')' . $suffix; + $this->publisher->publish($this->caller->stack( + array_merge($route->getMiddleware(), [$route->getController()]), + $request + )); } /** - * Define a route parameter pattern + * Define a parameter pattern * * @param string $name * @param string $pattern - * @return self */ - public function pattern(string $name, string $pattern): self + public function pattern(string $name, string $pattern) { $this->patterns[$name] = $pattern; - - return $this; - } - - /** - * Generate URL for given route name - * - * @param string $routeName - * @param string[] $parameters - * @return string - * @throws UndefinedRouteException - */ - public function url(string $routeName, array $parameters = []): string - { - if (!($route = $this->routeManager->getRepository()->findByName($routeName))) { - throw new UndefinedRouteException("There is no route with name `$routeName`."); - } - - $uri = $route->getPath(); - - foreach ($parameters as $name => $value) { - $uri = preg_replace('/\??{' . $name . '\??}/', $value, $uri); - } - - $uri = preg_replace('/{[^}]+\?}/', '', $uri); - $uri = str_replace('/?', '', $uri); - - return $uri; - } - - /** - * Setup IoC container - */ - private function setupContainer(): void - { - $this->container = new Container(); - - $this->container->singleton(Router::class, $this); - - $this->container->singleton(Container::class, $this->container); - $this->container->singleton(ContainerInterface::class, $this->container); - - $this->container->singleton(Publisher::class, HttpPublisher::class); - - $request = ServerRequestFactory::fromGlobals(); - $this->container->singleton(ServerRequestInterface::class, $request); - $this->container->singleton(ServerRequest::class, $request); } /** @@ -481,4 +281,68 @@ public function getContainer(): Container { return $this->container; } + + /** + * @return Storekeeper + */ + public function getStorekeeper(): Storekeeper + { + return $this->storekeeper; + } + + /** + * @param Storekeeper $storekeeper + */ + public function setStorekeeper(Storekeeper $storekeeper): void + { + $this->storekeeper = $storekeeper; + } + + /** + * @return Matcher + */ + public function getMatcher(): Matcher + { + return $this->matcher; + } + + /** + * @param Matcher $matcher + */ + public function setMatcher(Matcher $matcher): void + { + $this->matcher = $matcher; + } + + /** + * @return Caller + */ + public function getCaller(): Caller + { + return $this->caller; + } + + /** + * @param Caller $caller + */ + public function setCaller(Caller $caller): void + { + $this->caller = $caller; + } + + /** + * @return Publisher + */ + public function getPublisher(): Publisher + { + return $this->publisher; + } + + /** + * @param Publisher $publisher + */ + public function setPublisher(Publisher $publisher): void + { + $this->publisher = $publisher; + } } diff --git a/src/Routes/Route.php b/src/Routes/Route.php index 64bd8d7..fb7b18b 100644 --- a/src/Routes/Route.php +++ b/src/Routes/Route.php @@ -4,11 +4,6 @@ use Closure; -/** - * Class Route - * - * @package MiladRahimi\PhpRouter - */ class Route { /** diff --git a/src/Routes/RouteManager.php b/src/Routes/RouteManager.php deleted file mode 100644 index abb2bf3..0000000 --- a/src/Routes/RouteManager.php +++ /dev/null @@ -1,101 +0,0 @@ - '', - Attributes::MIDDLEWARE => [], - Attributes::DOMAIN => null, - ]; - - /** - * RouteManager constructor. - * - * @param RouteRepository $repository - */ - public function __construct(RouteRepository $repository) - { - $this->repository = $repository; - } - - /** - * Add a route to the collection - * - * @param string $method - * @param string $path - * @param $controller - * @param string|null $name - */ - public function add(string $method, string $path, $controller, ?string $name = null): void - { - $this->repository->save( - $method, - $this->attributes[Attributes::PREFIX] . $path, - $controller, - $name, - $this->attributes[Attributes::MIDDLEWARE], - $this->attributes[Attributes::DOMAIN] - ); - } - - /** - * Append new attributes to the existing ones - * - * @param array $attributes - */ - public function appendAttributes(array $attributes): void - { - $this->attributes[Attributes::DOMAIN] = $attributes[Attributes::DOMAIN] ?? null; - $this->attributes[Attributes::PREFIX] .= $attributes[Attributes::PREFIX] ?? ''; - $this->attributes[Attributes::MIDDLEWARE] = array_merge( - $this->attributes[Attributes::MIDDLEWARE], - $attributes[Attributes::MIDDLEWARE] ?? [] - ); - } - - /** - * @return array - */ - public function getAttributes(): array - { - return $this->attributes; - } - - /** - * @param array $attributes - */ - public function setAttributes(array $attributes): void - { - $this->attributes = $attributes; - } - - /** - * @return RouteRepository - */ - public function getRepository(): RouteRepository - { - return $this->repository; - } - - /** - * @param RouteRepository $repository - */ - public function setRepository(RouteRepository $repository): void - { - $this->repository = $repository; - } -} diff --git a/src/Routes/RouteRepository.php b/src/Routes/Store.php similarity index 93% rename from src/Routes/RouteRepository.php rename to src/Routes/Store.php index 93a5f1f..e4eda70 100644 --- a/src/Routes/RouteRepository.php +++ b/src/Routes/Store.php @@ -2,12 +2,7 @@ namespace MiladRahimi\PhpRouter\Routes; -/** - * Class RouteRepository - * - * @package MiladRahimi\PhpRouter - */ -class RouteRepository +class Store { /** * @var array diff --git a/src/Routes/Storekeeper.php b/src/Routes/Storekeeper.php new file mode 100644 index 0000000..917aeeb --- /dev/null +++ b/src/Routes/Storekeeper.php @@ -0,0 +1,102 @@ +repository = $repository; + } + + /** + * Add a route to the collection + * + * @param string $method + * @param string $path + * @param $controller + * @param string|null $name + */ + public function add(string $method, string $path, $controller, ?string $name = null): void + { + $path = $this->prefix . $path; + $this->repository->save($method, $path, $controller, $name, $this->middleware, $this->domain); + } + + /** + * @param array $attributes + */ + public function updateAttributes(array $attributes): void + { + $this->domain = $attributes[Attributes::DOMAIN]; + $this->prefix = $attributes[Attributes::PREFIX]; + $this->middleware = $attributes[Attributes::MIDDLEWARE]; + } + + /** + * Append new attributes to the existing ones + * + * @param array $attributes + */ + public function appendAttributes(array $attributes): void + { + $this->domain = $attributes[Attributes::DOMAIN] ?? null; + $this->prefix .= $attributes[Attributes::PREFIX] ?? ''; + $this->middleware = array_merge($this->middleware, $attributes[Attributes::MIDDLEWARE] ?? []); + } + + /** + * @return array + */ + public function attributes(): array + { + return [ + Attributes::DOMAIN => $this->domain, + Attributes::PREFIX => $this->prefix, + Attributes::MIDDLEWARE => $this->middleware, + ]; + } + + /** + * @return Store + */ + public function getRepository(): Store + { + return $this->repository; + } + + /** + * @param Store $repository + */ + public function setRepository(Store $repository): void + { + $this->repository = $repository; + } +} diff --git a/src/Url.php b/src/Url.php new file mode 100644 index 0000000..31696ac --- /dev/null +++ b/src/Url.php @@ -0,0 +1,50 @@ +store = $store; + } + + /** + * Generate URL for given route name + * + * @param string $routeName + * @param string[] $parameters + * @return string + * @throws UndefinedRouteException + */ + public function make(string $routeName, array $parameters = []): string + { + if (!($route = $this->store->findByName($routeName))) { + throw new UndefinedRouteException("There is no route named `$routeName`."); + } + + $uri = $route->getPath(); + + foreach ($parameters as $name => $value) { + $uri = preg_replace('/\??{' . $name . '\??}/', $value, $uri); + } + + $uri = preg_replace('/{[^}]+\?}/', '', $uri); + $uri = str_replace('/?', '', $uri); + + return $uri; + } +} diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 6eef73e..1f1c77a 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -15,9 +15,8 @@ class ContainerTest extends TestCase */ public function test_setting_and_getting_container() { - $router = new Router(); + $router = $this->router(); $router->getContainer()->singleton('name', 'Pink Floyd');; - $router->getContainer()->singleton(Publisher::class, new FakePublisher());; $router->get('/', function (Container $container) { return $container->get('name'); diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 6e3b90c..9a0ee0b 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -304,7 +304,7 @@ public function test_default_publisher() { ob_start(); - $router = new Router(); + $router = Router::create(); $router->get('/', function () { return 'home'; diff --git a/tests/TestCase.php b/tests/TestCase.php index 2eff41c..f7afe45 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,10 +3,10 @@ namespace MiladRahimi\PhpRouter\Tests; use Closure; +use MiladRahimi\PhpContainer\Exceptions\ContainerException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; -use MiladRahimi\PhpRouter\Attributes; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -39,13 +39,13 @@ protected function mockRequest(string $method, string $url): void /** * Get a router instance for testing purposes * - * @param Attributes|null $config * @return Router + * @throws ContainerException */ - protected function router(Attributes $config = null): Router + protected function router(): Router { - $router = new Router($config); - $router->getContainer()->singleton(Publisher::class, new FakePublisher()); + $router = Router::create(); + $router->setPublisher(new FakePublisher()); return $router; } @@ -70,7 +70,7 @@ protected function OkController(): Closure */ protected function output(Router $router) { - return $router->getContainer()->get(Publisher::class)->output; + return $this->publisher($router)->output; } /** @@ -81,7 +81,7 @@ protected function output(Router $router) */ protected function publisher(Router $router): FakePublisher { - return $router->getContainer()->get(Publisher::class); + return $router->getPublisher(); } /** diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 74dd6d0..842630b 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -4,6 +4,7 @@ use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; use MiladRahimi\PhpRouter\Router; +use MiladRahimi\PhpRouter\Url; use Throwable; class UrlTest extends TestCase @@ -14,8 +15,8 @@ class UrlTest extends TestCase public function test_generating_url_for_the_homepage() { $router = $this->router(); - $router->get('/', function (Router $r) { - return $r->url('home'); + $router->get('/', function (Url $r) { + return $r->make('home'); }, 'home'); $router->dispatch(); @@ -30,11 +31,11 @@ public function test_generating_url_for_a_page() $this->mockRequest('GET', 'http://web.com/page'); $router = $this->router(); - $router->get('/', function (Router $r) { - return $r->url('home'); + $router->get('/', function (Url $r) { + return $r->make('home'); }, 'home'); - $router->get('/page', function (Router $r) { - return $r->url('page'); + $router->get('/page', function (Url $r) { + return $r->make('page'); }, 'page'); $router->dispatch(); @@ -49,8 +50,8 @@ public function test_generating_url_for_a_page_with_required_parameter() $this->mockRequest('GET', 'http://web.com/contact'); $router = $this->router(); - $router->get('/{name}', function (Router $r) { - return $r->url('page', ['name' => 'about']); + $router->get('/{name}', function (Url $r) { + return $r->make('page', ['name' => 'about']); }, 'page'); $router->dispatch(); @@ -65,8 +66,8 @@ public function test_generating_url_for_a_page_with_optional_parameter() $this->mockRequest('GET', 'http://web.com/contact'); $router = $this->router(); - $router->get('/{name?}', function (Router $r) { - return $r->url('page', ['name' => 'about']); + $router->get('/{name?}', function (Url $r) { + return $r->make('page', ['name' => 'about']); }, 'page'); $router->dispatch(); @@ -81,8 +82,8 @@ public function test_generating_url_for_a_page_with_optional_parameter_2() $this->mockRequest('GET', 'http://web.com/contact'); $router = $this->router(); - $router->get('/{name?}', function (Router $r) { - return $r->url('page'); + $router->get('/{name?}', function (Url $r) { + return $r->make('page'); }, 'page'); $router->dispatch(); @@ -97,8 +98,8 @@ public function test_generating_url_for_a_page_with_optional_parameter_3() $this->mockRequest('GET', 'http://web.com/page/contact'); $router = $this->router(); - $router->get('/page/?{name?}', function (Router $r) { - return $r->url('page'); + $router->get('/page/?{name?}', function (Url $r) { + return $r->make('page'); }, 'page'); $router->dispatch(); @@ -111,11 +112,11 @@ public function test_generating_url_for_a_page_with_optional_parameter_3() public function test_generating_url_for_undefined_route() { $this->expectException(UndefinedRouteException::class); - $this->expectExceptionMessage("There is no route with name `home`."); + $this->expectExceptionMessage("There is no route named `home`."); $router = $this->router(); - $router->get('/', function (Router $r) { - return $r->url('home'); + $router->get('/', function (Url $r) { + return $r->make('home'); }); $router->dispatch(); } From 0389634ec9dc8f3673823deff574fb91136605bf Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Fri, 27 Nov 2020 23:28:56 +0330 Subject: [PATCH 29/52] v5 --- src/Dispatching/Caller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dispatching/Caller.php b/src/Dispatching/Caller.php index ec99c7f..e8af32c 100644 --- a/src/Dispatching/Caller.php +++ b/src/Dispatching/Caller.php @@ -75,7 +75,7 @@ public function call($callable) throw new InvalidCallableException("Class `$callable` not found."); } - $object = new $class(); + $object = $this->container->instantiate($class); if (method_exists($object, $method) == false) { throw new InvalidCallableException("Method `$class::$method` not found."); From ddf6d26319e17d9fcc45b6748370cf3a9cdd114b Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 28 Nov 2020 00:26:28 +0330 Subject: [PATCH 30/52] v5 --- tests/ContainerTest.php | 2 +- tests/TestCase.php | 8 +-- .../{FakePublisher.php => TrapPublisher.php} | 2 +- tests/UrlTest.php | 63 +++++++++++-------- 4 files changed, 43 insertions(+), 32 deletions(-) rename tests/Testing/{FakePublisher.php => TrapPublisher.php} (96%) diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 1f1c77a..1c40735 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -5,7 +5,7 @@ use MiladRahimi\PhpContainer\Container; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; -use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; +use MiladRahimi\PhpRouter\Tests\Testing\TrapPublisher; use Throwable; class ContainerTest extends TestCase diff --git a/tests/TestCase.php b/tests/TestCase.php index f7afe45..f46c761 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,7 +6,7 @@ use MiladRahimi\PhpContainer\Exceptions\ContainerException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; -use MiladRahimi\PhpRouter\Tests\Testing\FakePublisher; +use MiladRahimi\PhpRouter\Tests\Testing\TrapPublisher; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -45,7 +45,7 @@ protected function mockRequest(string $method, string $url): void protected function router(): Router { $router = Router::create(); - $router->setPublisher(new FakePublisher()); + $router->setPublisher(new TrapPublisher()); return $router; } @@ -77,9 +77,9 @@ protected function output(Router $router) * Get the given router publisher. * * @param Router $router - * @return FakePublisher|Publisher + * @return TrapPublisher|Publisher */ - protected function publisher(Router $router): FakePublisher + protected function publisher(Router $router): TrapPublisher { return $router->getPublisher(); } diff --git a/tests/Testing/FakePublisher.php b/tests/Testing/TrapPublisher.php similarity index 96% rename from tests/Testing/FakePublisher.php rename to tests/Testing/TrapPublisher.php index 9027f97..14fd755 100644 --- a/tests/Testing/FakePublisher.php +++ b/tests/Testing/TrapPublisher.php @@ -5,7 +5,7 @@ use MiladRahimi\PhpRouter\Services\Publisher; use Psr\Http\Message\ResponseInterface; -class FakePublisher implements Publisher +class TrapPublisher implements Publisher { /** * @var string diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 842630b..7eb706a 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -3,7 +3,6 @@ namespace MiladRahimi\PhpRouter\Tests; use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; -use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Url; use Throwable; @@ -15,8 +14,8 @@ class UrlTest extends TestCase public function test_generating_url_for_the_homepage() { $router = $this->router(); - $router->get('/', function (Url $r) { - return $r->make('home'); + $router->get('/', function (Url $url) { + return $url->make('home'); }, 'home'); $router->dispatch(); @@ -31,11 +30,11 @@ public function test_generating_url_for_a_page() $this->mockRequest('GET', 'http://web.com/page'); $router = $this->router(); - $router->get('/', function (Url $r) { - return $r->make('home'); + $router->get('/', function (Url $url) { + return $url->make('home'); }, 'home'); - $router->get('/page', function (Url $r) { - return $r->make('page'); + $router->get('/page', function (Url $url) { + return $url->make('page'); }, 'page'); $router->dispatch(); @@ -47,11 +46,14 @@ public function test_generating_url_for_a_page() */ public function test_generating_url_for_a_page_with_required_parameter() { - $this->mockRequest('GET', 'http://web.com/contact'); + $this->mockRequest('GET', 'http://web.com/'); $router = $this->router(); - $router->get('/{name}', function (Url $r) { - return $r->make('page', ['name' => 'about']); + $router->get('/', function (Url $url) { + return $url->make('page', ['name' => 'about']); + }); + $router->get('/{name}', function () { + return 'empty'; }, 'page'); $router->dispatch(); @@ -63,47 +65,56 @@ public function test_generating_url_for_a_page_with_required_parameter() */ public function test_generating_url_for_a_page_with_optional_parameter() { - $this->mockRequest('GET', 'http://web.com/contact'); + $this->mockRequest('GET', 'http://web.com/'); $router = $this->router(); - $router->get('/{name?}', function (Url $r) { - return $r->make('page', ['name' => 'about']); + $router->get('/', function (Url $url) { + return $url->make('page', ['name' => 'blog']); + }); + $router->get('/{name?}', function () { + return 'empty'; }, 'page'); $router->dispatch(); - $this->assertEquals('/about', $this->output($router)); + $this->assertEquals('/blog', $this->output($router)); } /** * @throws Throwable */ - public function test_generating_url_for_a_page_with_optional_parameter_2() + public function test_generating_url_for_a_page_with_optional_parameter_ignored() { - $this->mockRequest('GET', 'http://web.com/contact'); + $this->mockRequest('GET', 'http://web.com/'); $router = $this->router(); - $router->get('/{name?}', function (Url $r) { - return $r->make('page'); + $router->get('/', function (Url $url) { + return $url->make('page'); + }); + $router->get('/profile/{name?}', function () { + return 'empty'; }, 'page'); $router->dispatch(); - $this->assertEquals('/', $this->output($router)); + $this->assertEquals('/profile/', $this->output($router)); } /** * @throws Throwable */ - public function test_generating_url_for_a_page_with_optional_parameter_3() + public function test_generating_url_for_a_page_with_optional_parameter_and_slash_ignored() { - $this->mockRequest('GET', 'http://web.com/page/contact'); + $this->mockRequest('GET', 'http://web.com/'); $router = $this->router(); - $router->get('/page/?{name?}', function (Url $r) { - return $r->make('page'); + $router->get('/', function (Url $url) { + return $url->make('page'); + }); + $router->get('/profile/?{name?}', function () { + return 'empty'; }, 'page'); $router->dispatch(); - $this->assertEquals('/page', $this->output($router)); + $this->assertEquals('/profile', $this->output($router)); } /** @@ -112,11 +123,11 @@ public function test_generating_url_for_a_page_with_optional_parameter_3() public function test_generating_url_for_undefined_route() { $this->expectException(UndefinedRouteException::class); - $this->expectExceptionMessage("There is no route named `home`."); + $this->expectExceptionMessage("There is no route named `page`."); $router = $this->router(); $router->get('/', function (Url $r) { - return $r->make('home'); + return $r->make('page'); }); $router->dispatch(); } From 391e844c2dc70a1bacfe734a29582b92d149877b Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 30 Nov 2020 21:23:08 +0330 Subject: [PATCH 31/52] v5 --- src/Router.php | 1 + src/Routes/Store.php | 4 + src/Routes/Storekeeper.php | 16 +-- tests/ControllerTest.php | 79 ++++++++++++++ tests/HttpMethodTest.php | 170 +++++++++++++++++++++++++++++ tests/RoutingTest.php | 130 ---------------------- tests/Testing/SampleController.php | 5 + 7 files changed, 267 insertions(+), 138 deletions(-) create mode 100644 tests/ControllerTest.php create mode 100644 tests/HttpMethodTest.php diff --git a/src/Router.php b/src/Router.php index da9776e..c5348be 100644 --- a/src/Router.php +++ b/src/Router.php @@ -135,6 +135,7 @@ public function map(string $method, string $path, $controller, ?string $name = n /** * Dispatch routes and run the application * + * @throws ContainerException * @throws InvalidCallableException * @throws RouteNotFoundException */ diff --git a/src/Routes/Store.php b/src/Routes/Store.php index e4eda70..cc336b6 100644 --- a/src/Routes/Store.php +++ b/src/Routes/Store.php @@ -50,8 +50,12 @@ public function findByMethod(string $method): array $this->repository['method'][$method] ?? [] ); + print_r($routes); + sort($routes, SORT_DESC); + print_r($routes); + return $routes; } diff --git a/src/Routes/Storekeeper.php b/src/Routes/Storekeeper.php index 917aeeb..6b46603 100644 --- a/src/Routes/Storekeeper.php +++ b/src/Routes/Storekeeper.php @@ -9,7 +9,7 @@ class Storekeeper /** * @var Store */ - private $repository; + private $store; /** * @var string @@ -33,7 +33,7 @@ class Storekeeper */ public function __construct(Store $repository) { - $this->repository = $repository; + $this->store = $repository; } /** @@ -47,7 +47,7 @@ public function __construct(Store $repository) public function add(string $method, string $path, $controller, ?string $name = null): void { $path = $this->prefix . $path; - $this->repository->save($method, $path, $controller, $name, $this->middleware, $this->domain); + $this->store->save($method, $path, $controller, $name, $this->middleware, $this->domain); } /** @@ -87,16 +87,16 @@ public function attributes(): array /** * @return Store */ - public function getRepository(): Store + public function getStore(): Store { - return $this->repository; + return $this->store; } /** - * @param Store $repository + * @param Store $store */ - public function setRepository(Store $repository): void + public function setStore(Store $store): void { - $this->repository = $repository; + $this->store = $store; } } diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php new file mode 100644 index 0000000..8881225 --- /dev/null +++ b/tests/ControllerTest.php @@ -0,0 +1,79 @@ +router(); + $router->get('/', function () { + return 'Closure'; + }); + $router->dispatch(); + + $this->assertEquals('Closure', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_a_class_method_controller() + { + $router = $this->router(); + $router->get('/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_a_function_controller_it_is_deprecated_and_should_fail() + { + function home() { + return 'Function'; + } + + $router = $this->router(); + $router->get('/', 'home'); + + $this->expectException(InvalidCallableException::class); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_an_invalid_array_as_controller_it_should_fail() + { + $router = $this->router(); + $router->get('/', ['invalid', 'array', 'controller']); + + $this->expectException(InvalidCallableException::class); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_multiple_controller_for_the_same_route_it_should_call_the_last_one() + { + $router = $this->router(); + $router->get('/', [SampleController::class, 'page']); + $router->get('/', [SampleController::class, 'home']); + $router->dispatch(); + + print_r($router->getStorekeeper()->getStore()->findByMethod('GET')); + + $this->assertEquals('Page', $this->output($router)); + } +} diff --git a/tests/HttpMethodTest.php b/tests/HttpMethodTest.php new file mode 100644 index 0000000..55543cb --- /dev/null +++ b/tests/HttpMethodTest.php @@ -0,0 +1,170 @@ +mockRequest('GET', 'http://example.com/'); + + $router = $this->router(); + $router->get('/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_a_post_route() + { + $this->mockRequest('POST', 'http://example.com/'); + + $router = $this->router(); + $router->post('/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_a_put_route() + { + $this->mockRequest('PUT', 'http://example.com/'); + + $router = $this->router(); + $router->put('/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_a_patch_route() + { + $this->mockRequest('PATCH', 'http://example.com/'); + + $router = $this->router(); + $router->patch('/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_a_delete_route() + { + $this->mockRequest('DELETE', 'http://example.com/'); + + $router = $this->router(); + $router->delete('/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_an_options_route() + { + $this->mockRequest('OPTIONS', 'http://example.com/'); + + $router = $this->router(); + $router->delete('/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_map_a_post_route() + { + $this->mockRequest('POST', 'http://example.com/'); + + $router = $this->router(); + $router->map('POST', '/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_map_a_custom_method() + { + $this->mockRequest('CUSTOM', 'http://example.com/'); + + $router = $this->router(); + $router->map('CUSTOM', '/', [SampleController::class, 'home']); + $router->dispatch(); + + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_match_multiple_routes() + { + $router = $this->router(); + $router->match(['PUT', 'PATCH'], '/', [SampleController::class, 'home']); + + $this->mockRequest('PUT', 'http://example.com/'); + $router->dispatch(); + $this->assertEquals('Home', $this->output($router)); + + $this->mockRequest('PATCH', 'http://example.com/'); + $router->dispatch(); + $this->assertEquals('Home', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_match_no_route() + { + $this->mockRequest('GET', 'http://example.com/'); + + $router = $this->router(); + $router->match([], '/', [SampleController::class, 'home']); + + $this->expectException(RouteNotFoundException::class); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_any_with_some_methods() + { + $router = $this->router(); + $router->any('/', [SampleController::class, 'home']); + + $this->mockRequest('GET', 'http://example.com/'); + $router->dispatch(); + $this->assertEquals('Home', $this->output($router)); + + $this->mockRequest('POST', 'http://example.com/'); + $router->dispatch(); + $this->assertEquals('Home', $this->output($router)); + } +} diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 9a0ee0b..53b50b0 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -13,136 +13,6 @@ class RoutingTest extends TestCase { - /** - * @throws Throwable - */ - public function test_a_simple_get_route() - { - $this->mockRequest('GET', 'http://example.com/'); - - $router = $this->router(); - $router->map('GET', '/', $this->OkController()); - $router->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_a_simple_post_route() - { - $this->mockRequest('POST', 'http://example.com/'); - - $router = $this->router(); - $router->post('/', $this->OkController()); - $router->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_a_simple_put_route() - { - $this->mockRequest('PUT', 'http://example.com/'); - - $router = $this->router(); - $router->put('/', $this->OkController()); - $router->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_a_simple_patch_route() - { - $this->mockRequest('PATCH', 'http://example.com/'); - - $router = $this->router(); - $router->patch('/', $this->OkController()); - $router->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_a_simple_delete_route() - { - $this->mockRequest('DELETE', 'http://example.com/'); - - $router = $this->router(); - $router->delete('/', $this->OkController()); - $router->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_a_route_with_custom_method() - { - $method = "SCREW"; - - $this->mockRequest($method, 'http://example.com/'); - - $router = $this->router(); - $router->map($method, '/', $this->OkController()); - $router->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_the_any_method() - { - $this->mockRequest('GET', 'http://example.com/'); - - $router = $this->router(); - $router->any('/', function () { - return 'Test any for get'; - }); - $router->dispatch(); - - $this->assertEquals('Test any for get', $this->output($router)); - - $this->mockRequest('POST', 'http://example.com/'); - - $router = $this->router(); - $router->any('/', function () { - return 'Test any for post'; - }); - $router->dispatch(); - - $this->assertEquals('Test any for post', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_multiple_routes() - { - $this->mockRequest('POST', 'http://example.com/666'); - - $router = $this->router(); - $router->get('/', function () { - return 'Home'; - }); - $router->post('/{id}', function ($id) { - return $id; - }); - $router->dispatch(); - - $this->assertEquals('666', $this->output($router)); - } /** * @throws Throwable diff --git a/tests/Testing/SampleController.php b/tests/Testing/SampleController.php index 0e18ea8..8fababd 100644 --- a/tests/Testing/SampleController.php +++ b/tests/Testing/SampleController.php @@ -11,4 +11,9 @@ public function home() { return 'Home'; } + + public function page() + { + return 'Page'; + } } From f358c82867aab158a940257cc65e86597bdae267 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Mon, 30 Nov 2020 22:57:01 +0330 Subject: [PATCH 32/52] update --- src/Routes/Store.php | 6 +----- tests/ControllerTest.php | 4 +--- tests/HttpMethodTest.php | 2 +- tests/UrlTest.php | 8 ++++---- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Routes/Store.php b/src/Routes/Store.php index cc336b6..1012c99 100644 --- a/src/Routes/Store.php +++ b/src/Routes/Store.php @@ -50,11 +50,7 @@ public function findByMethod(string $method): array $this->repository['method'][$method] ?? [] ); - print_r($routes); - - sort($routes, SORT_DESC); - - print_r($routes); + krsort($routes); return $routes; } diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 8881225..3b46a3a 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -68,12 +68,10 @@ public function test_an_invalid_array_as_controller_it_should_fail() public function test_multiple_controller_for_the_same_route_it_should_call_the_last_one() { $router = $this->router(); - $router->get('/', [SampleController::class, 'page']); $router->get('/', [SampleController::class, 'home']); + $router->get('/', [SampleController::class, 'page']); $router->dispatch(); - print_r($router->getStorekeeper()->getStore()->findByMethod('GET')); - $this->assertEquals('Page', $this->output($router)); } } diff --git a/tests/HttpMethodTest.php b/tests/HttpMethodTest.php index 55543cb..7dc792d 100644 --- a/tests/HttpMethodTest.php +++ b/tests/HttpMethodTest.php @@ -86,7 +86,7 @@ public function test_an_options_route() $this->mockRequest('OPTIONS', 'http://example.com/'); $router = $this->router(); - $router->delete('/', [SampleController::class, 'home']); + $router->options('/', [SampleController::class, 'home']); $router->dispatch(); $this->assertEquals('Home', $this->output($router)); diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 7eb706a..204176f 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -69,14 +69,14 @@ public function test_generating_url_for_a_page_with_optional_parameter() $router = $this->router(); $router->get('/', function (Url $url) { - return $url->make('page', ['name' => 'blog']); + return $url->make('post', ['post' => 666]); }); - $router->get('/{name?}', function () { + $router->get('/blog/{post?}', function () { return 'empty'; - }, 'page'); + }, 'post'); $router->dispatch(); - $this->assertEquals('/blog', $this->output($router)); + $this->assertEquals('/blog/666', $this->output($router)); } /** From 2ff7c364d6dadfbb1f6b7029c66f86907eebd58e Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Tue, 1 Dec 2020 02:13:28 +0330 Subject: [PATCH 33/52] update --- tests/HttpMethodTest.php | 9 ++- tests/InjectionTest.php | 83 +++++++++++++++++++++++++ tests/ParametersTest.php | 128 +++++++++++++++++++++++++++++++++++++++ tests/PatternsTest.php | 116 +++++++++++++++++++++++++++++++++++ tests/RoutingTest.php | 112 ---------------------------------- 5 files changed, 333 insertions(+), 115 deletions(-) create mode 100644 tests/InjectionTest.php create mode 100644 tests/ParametersTest.php create mode 100644 tests/PatternsTest.php diff --git a/tests/HttpMethodTest.php b/tests/HttpMethodTest.php index 7dc792d..15bc60f 100644 --- a/tests/HttpMethodTest.php +++ b/tests/HttpMethodTest.php @@ -2,6 +2,7 @@ namespace MiladRahimi\PhpRouter\Tests; +use Laminas\Diactoros\ServerRequest; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Tests\Testing\SampleController; use Throwable; @@ -157,14 +158,16 @@ public function test_match_no_route() public function test_any_with_some_methods() { $router = $this->router(); - $router->any('/', [SampleController::class, 'home']); + $router->any('/', function (ServerRequest $request) { + return $request->getMethod(); + }); $this->mockRequest('GET', 'http://example.com/'); $router->dispatch(); - $this->assertEquals('Home', $this->output($router)); + $this->assertEquals('GET', $this->output($router)); $this->mockRequest('POST', 'http://example.com/'); $router->dispatch(); - $this->assertEquals('Home', $this->output($router)); + $this->assertEquals('POST', $this->output($router)); } } diff --git a/tests/InjectionTest.php b/tests/InjectionTest.php new file mode 100644 index 0000000..9bcca28 --- /dev/null +++ b/tests/InjectionTest.php @@ -0,0 +1,83 @@ +router(); + $router->get('/', function (ServerRequest $request) { + return get_class($request); + }); + $router->dispatch(); + + $this->assertEquals(ServerRequest::class, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_injecting_request_by_interface() + { + $router = $this->router(); + $router->get('/', function (ServerRequestInterface $request) { + return get_class($request); + }); + $router->dispatch(); + + $this->assertEquals(ServerRequest::class, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_injecting_route() + { + $router = $this->router(); + $router->get('/', function (Route $route) { + return $route->getPath(); + }); + $router->dispatch(); + + $this->assertEquals('/', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_injecting_container() + { + $router = $this->router(); + $router->get('/', function (Container $container) { + return get_class($container); + }); + $router->dispatch(); + + $this->assertEquals(Container::class, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_injecting_container_by_interface() + { + $router = $this->router(); + $router->get('/', function (ContainerInterface $container) { + return get_class($container); + }); + $router->dispatch(); + + $this->assertEquals(Container::class, $this->output($router)); + } +} diff --git a/tests/ParametersTest.php b/tests/ParametersTest.php new file mode 100644 index 0000000..c42259f --- /dev/null +++ b/tests/ParametersTest.php @@ -0,0 +1,128 @@ +mockRequest('GET', "http://example.com/products/$id"); + + $router = $this->router(); + $router->get('/products/{id}', function ($id) { + return $id; + }); + $router->dispatch(); + + $this->assertEquals($id, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_some_required_parameters() + { + $pid = random_int(1, 100); + $cid = random_int(1, 100); + $this->mockRequest('GET', "http://example.com/products/$pid/comments/$cid"); + + $router = $this->router(); + $router->get('/products/{pid}/comments/{cid}', function ($pid, $cid) { + return $pid . $cid; + }); + $router->dispatch(); + + $this->assertEquals($pid . $cid, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_a_provided_optional_parameter() + { + $id = random_int(1, 100); + $this->mockRequest('GET', "http://example.com/products/$id"); + + $router = $this->router(); + $router->get('/products/{id?}', function ($id = 'default') { + return $id; + }); + $router->dispatch(); + + $this->assertEquals($id, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_a_unprovided_optional_parameter() + { + $this->mockRequest('GET', "http://example.com/products/"); + + $router = $this->router(); + $router->get('/products/{id?}', function ($id = 'default') { + return $id; + }); + $router->dispatch(); + + $this->assertEquals('default', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_a_unprovided_optional_parameter_and_slash() + { + $this->mockRequest('GET', "http://example.com/products"); + + $router = $this->router(); + $router->get('/products/?{id?}', function ($id = 'default') { + return $id; + }); + $router->dispatch(); + + $this->assertEquals('default', $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_some_optional_parameters() + { + $pid = random_int(1, 100); + $cid = random_int(1, 100); + $this->mockRequest('GET', "http://example.com/products/$pid/comments/$cid"); + + $router = $this->router(); + $router->get('/products/{pid?}/comments/{cid?}', function ($pid, $cid) { + return $pid . $cid; + }); + $router->dispatch(); + + $this->assertEquals($pid . $cid, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_mixin_parameters() + { + $pid = random_int(1, 100); + $cid = random_int(1, 100); + $this->mockRequest('GET', "http://example.com/products/$pid/comments/$cid"); + + $router = $this->router(); + $router->get('/products/{pid}/comments/{cid?}', function ($pid, $cid = 'default') { + return $pid . $cid; + }); + $router->dispatch(); + + $this->assertEquals($pid . $cid, $this->output($router)); + } +} \ No newline at end of file diff --git a/tests/PatternsTest.php b/tests/PatternsTest.php new file mode 100644 index 0000000..094a0a7 --- /dev/null +++ b/tests/PatternsTest.php @@ -0,0 +1,116 @@ +mockRequest('GET', "http://example.com/products/$id"); + + $router = $this->router(); + $router->pattern('id', '[0-9]'); + $router->get('/products/{id}', function ($id) { + return $id; + }); + $router->dispatch(); + + $this->assertEquals($id, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_a_single_digit_pattern_given_more() + { + $id = random_int(10, 100); + $this->mockRequest('GET', "http://example.com/products/$id"); + + $router = $this->router(); + $router->pattern('id', '[0-9]'); + $router->get('/products/{id}', function ($id) { + return $id; + }); + + $this->expectException(RouteNotFoundException::class); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_with_a_multi_digits_pattern() + { + $id = random_int(10, 100); + $this->mockRequest('GET', "http://example.com/products/$id"); + + $router = $this->router(); + $router->pattern('id', '[0-9]+'); + $router->get('/products/{id}', function ($id) { + return $id; + }); + $router->dispatch(); + + $this->assertEquals($id, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_a_multi_digits_pattern_given_string() + { + $this->mockRequest('GET', "http://example.com/products/string"); + + $router = $this->router(); + $router->pattern('id', '[0-9]+'); + $router->get('/products/{id}', function ($id) { + return $id; + }); + + $this->expectException(RouteNotFoundException::class); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_with_a_alphanumeric_pattern() + { + $id = 'abc123xyz'; + $this->mockRequest('GET', "http://example.com/products/$id"); + + $router = $this->router(); + $router->pattern('id', '[0-9a-z]+'); + $router->get('/products/{id}', function ($id) { + return $id; + }); + $router->dispatch(); + + $this->assertEquals($id, $this->output($router)); + } + + /** + * @throws Throwable + */ + public function test_with_a_alphanumeric_pattern_given_invalid() + { + $id = 'abc$$$'; + $this->mockRequest('GET', "http://example.com/products/$id"); + + $router = $this->router(); + $router->pattern('id', '[0-9a-z]+'); + $router->get('/products/{id}', function ($id) { + return $id; + }); + + $this->expectException(RouteNotFoundException::class); + $router->dispatch(); + } +} diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 53b50b0..3731249 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -13,118 +13,6 @@ class RoutingTest extends TestCase { - - /** - * @throws Throwable - */ - public function test_duplicate_routes_with_different_controllers() - { - $router = $this->router(); - $router->get('/', function () { - return 'Home'; - }); - $router->get('/', function () { - return 'Home again!'; - }); - $router->dispatch(); - - $this->assertEquals('Home again!', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_multiple_http_methods() - { - $this->mockRequest('POST', 'http://example.com/'); - - $router = $this->router(); - $router->get('/', function () { - return 'Get'; - }); - $router->post('/', function () { - return 'Post'; - }); - $router->delete('/', function () { - return 'Delete'; - }); - $router->dispatch(); - - $this->assertEquals('Post', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_with_a_required_parameter() - { - $this->mockRequest('GET', 'http://web.com/666'); - - $router = $this->router(); - $router->get('/{id}', function ($id) { - return $id; - }); - $router->dispatch(); - - $this->assertEquals('666', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_with_a_optional_parameter_when_it_is_present() - { - $this->mockRequest('GET', 'http://web.com/666'); - - $router = $this->router(); - $router->get('/{id?}', function ($id) { - return $id; - }); - $router->dispatch(); - - $this->assertEquals('666', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_with_a_optional_parameter_when_it_is_not_present() - { - $this->mockRequest('GET', 'http://web.com/'); - - $router = $this->router(); - $router->get('/{id?}', function ($id = 'Default') { - return $id; - }); - $router->dispatch(); - - $this->assertEquals('Default', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_with_defined_parameters() - { - $this->mockRequest('GET', 'http://example.com/666'); - - $router = $this->router(); - $router->pattern('id', '[0-9]+'); - $router->get('/{id}', $this->OkController()); - $router->dispatch(); - - $this->assertEquals('OK', $this->output($router)); - - $this->mockRequest('GET', 'http://example.com/abc'); - - $this->expectException(RouteNotFoundException::class); - - $router = $this->router(); - $router->pattern('id', '[0-9]+'); - $router->get('/{id}', $this->OkController()); - $router->dispatch(); - } - /** * @throws Throwable */ From adbcee49ec4fe33ed3c64f622beddb2ed7566539 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 9 Jan 2021 17:24:20 +0330 Subject: [PATCH 34/52] update --- composer.json | 4 +- src/Attributes.php | 10 -- src/Dispatching/Caller.php | 16 ++- src/Dispatching/Matcher.php | 16 +-- src/Router.php | 16 +-- src/Routes/Storekeeper.php | 102 ---------------- .../Store.php => Routing/Repository.php} | 18 +-- src/{Routes => Routing}/Route.php | 2 +- src/Routing/State.php | 114 ++++++++++++++++++ src/Routing/Storekeeper.php | 80 ++++++++++++ src/Url.php | 14 +-- tests/InjectionTest.php | 2 +- tests/NamingTest.php | 2 +- tests/RoutingTest.php | 2 +- tests/TestCase.php | 2 +- 15 files changed, 240 insertions(+), 160 deletions(-) delete mode 100644 src/Attributes.php delete mode 100644 src/Routes/Storekeeper.php rename src/{Routes/Store.php => Routing/Repository.php} (73%) rename src/{Routes => Routing}/Route.php (98%) create mode 100644 src/Routing/State.php create mode 100644 src/Routing/Storekeeper.php diff --git a/composer.json b/composer.json index 7c2d99d..0b9e5f4 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "miladrahimi/phprouter", - "description": "A powerful, lightweight, and very fast HTTP URL router.", + "description": "A powerful, lightweight, and very fast HTTP URL router for PHP projects.", "keywords": [ "Route", "Router", @@ -33,7 +33,7 @@ "miladrahimi/phpcontainer": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^7" + "phpunit/phpunit": "^7|^8" }, "autoload": { "psr-4": { diff --git a/src/Attributes.php b/src/Attributes.php deleted file mode 100644 index 4cd009a..0000000 --- a/src/Attributes.php +++ /dev/null @@ -1,10 +0,0 @@ -container->singleton(ServerRequest::class, $request); $this->container->singleton(ServerRequestInterface::class, $request); - if (isset($callables[$i + 1])) { - $next = function (ServerRequestInterface $request) use ($callables, $i) { - return $this->stack($callables, $request, $i + 1); - }; - - $this->container->closure('$next', $next); + if (isset($callables[$index + 1])) { + $this->container->closure('$next', function (ServerRequestInterface $request) use ($callables, $index) { + return $this->stack($callables, $request, $index + 1); + }); } else { $this->container->delete('$next'); } - return $this->call($callables[$i]); + return $this->call($callables[$index]); } /** diff --git a/src/Dispatching/Matcher.php b/src/Dispatching/Matcher.php index 333e56a..fc5822b 100644 --- a/src/Dispatching/Matcher.php +++ b/src/Dispatching/Matcher.php @@ -3,25 +3,25 @@ namespace MiladRahimi\PhpRouter\Dispatching; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; -use MiladRahimi\PhpRouter\Routes\Route; -use MiladRahimi\PhpRouter\Routes\Store; +use MiladRahimi\PhpRouter\Routing\Route; +use MiladRahimi\PhpRouter\Routing\Repository; use Psr\Http\Message\ServerRequestInterface; class Matcher { /** - * @var Store + * @var Repository */ - private $store; + private $repository; /** * Matcher constructor. * - * @param Store $store + * @param Repository $repository */ - public function __construct(Store $store) + public function __construct(Repository $repository) { - $this->store = $store; + $this->repository = $repository; } /** @@ -34,7 +34,7 @@ public function __construct(Store $store) */ public function find(ServerRequestInterface $request, array $patterns) { - foreach ($this->store->findByMethod($request->getMethod()) as $route) { + foreach ($this->repository->findByMethod($request->getMethod()) as $route) { $parameters = []; if ($this->compare($route, $request, $parameters, $patterns)) { diff --git a/src/Router.php b/src/Router.php index c5348be..7ab8ea1 100644 --- a/src/Router.php +++ b/src/Router.php @@ -9,9 +9,9 @@ use MiladRahimi\PhpRouter\Dispatching\Matcher; use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; -use MiladRahimi\PhpRouter\Routes\Route; -use MiladRahimi\PhpRouter\Routes\Storekeeper; -use MiladRahimi\PhpRouter\Routes\Store; +use MiladRahimi\PhpRouter\Routing\Route; +use MiladRahimi\PhpRouter\Routing\Storekeeper; +use MiladRahimi\PhpRouter\Routing\Repository; use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use Psr\Container\ContainerInterface; @@ -27,7 +27,7 @@ class Router private $container; /** - * The storekeeper of route store (repository) + * The storekeeper of route repository * * @var Storekeeper */ @@ -96,7 +96,7 @@ public static function create(): self $container = new Container(); $container->singleton(Container::class, $container); $container->singleton(ContainerInterface::class, $container); - $container->singleton(Store::class, new Store()); + $container->singleton(Repository::class, new Repository()); $container->singleton(Publisher::class, HttpPublisher::class); return $container->instantiate(Router::class); @@ -110,13 +110,13 @@ public static function create(): self */ public function group(array $attributes, Closure $body): void { - $oldAttributes = $this->storekeeper->attributes(); + $oldState = clone $this->storekeeper->getState(); - $this->storekeeper->appendAttributes($attributes); + $this->storekeeper->getState()->append($attributes); call_user_func($body, $this); - $this->storekeeper->updateAttributes($oldAttributes); + $this->storekeeper->setState($oldState); } /** diff --git a/src/Routes/Storekeeper.php b/src/Routes/Storekeeper.php deleted file mode 100644 index 6b46603..0000000 --- a/src/Routes/Storekeeper.php +++ /dev/null @@ -1,102 +0,0 @@ -store = $repository; - } - - /** - * Add a route to the collection - * - * @param string $method - * @param string $path - * @param $controller - * @param string|null $name - */ - public function add(string $method, string $path, $controller, ?string $name = null): void - { - $path = $this->prefix . $path; - $this->store->save($method, $path, $controller, $name, $this->middleware, $this->domain); - } - - /** - * @param array $attributes - */ - public function updateAttributes(array $attributes): void - { - $this->domain = $attributes[Attributes::DOMAIN]; - $this->prefix = $attributes[Attributes::PREFIX]; - $this->middleware = $attributes[Attributes::MIDDLEWARE]; - } - - /** - * Append new attributes to the existing ones - * - * @param array $attributes - */ - public function appendAttributes(array $attributes): void - { - $this->domain = $attributes[Attributes::DOMAIN] ?? null; - $this->prefix .= $attributes[Attributes::PREFIX] ?? ''; - $this->middleware = array_merge($this->middleware, $attributes[Attributes::MIDDLEWARE] ?? []); - } - - /** - * @return array - */ - public function attributes(): array - { - return [ - Attributes::DOMAIN => $this->domain, - Attributes::PREFIX => $this->prefix, - Attributes::MIDDLEWARE => $this->middleware, - ]; - } - - /** - * @return Store - */ - public function getStore(): Store - { - return $this->store; - } - - /** - * @param Store $store - */ - public function setStore(Store $store): void - { - $this->store = $store; - } -} diff --git a/src/Routes/Store.php b/src/Routing/Repository.php similarity index 73% rename from src/Routes/Store.php rename to src/Routing/Repository.php index 1012c99..c57a55b 100644 --- a/src/Routes/Store.php +++ b/src/Routing/Repository.php @@ -1,13 +1,13 @@ repository['method'][$method][] = $route; + $this->routes['method'][$method][] = $route; if ($name) { - $this->repository['name'][$name] = $route; + $this->routes['name'][$name] = $route; } } @@ -46,8 +46,8 @@ public function save( public function findByMethod(string $method): array { $routes = array_merge( - $this->repository['method']['*'] ?? [], - $this->repository['method'][$method] ?? [] + $this->routes['method']['*'] ?? [], + $this->routes['method'][$method] ?? [] ); krsort($routes); @@ -63,6 +63,6 @@ public function findByMethod(string $method): array */ public function findByName(string $name): ?Route { - return $this->repository['name'][$name] ?? null; + return $this->routes['name'][$name] ?? null; } } diff --git a/src/Routes/Route.php b/src/Routing/Route.php similarity index 98% rename from src/Routes/Route.php rename to src/Routing/Route.php index fb7b18b..9ee8f7f 100644 --- a/src/Routes/Route.php +++ b/src/Routing/Route.php @@ -1,6 +1,6 @@ prefix = $prefix; + $this->middleware = $middleware; + $this->domain = $domain; + } + + /** + * Create a state from the given array + * + * @param array $attributes + * @return self + */ + public static function createFromArray(array $attributes): self + { + return new static( + $attributes[static::PREFIX ?? ''], + $attributes[static::MIDDLEWARE ?? []], + $attributes[static::DOMAIN ?? null] + ); + } + + /** + * Append new attributes to the existing ones + * + * @param array $attributes + */ + public function append(array $attributes): void + { + $this->domain = $attributes[State::DOMAIN] ?? null; + $this->prefix .= $attributes[State::PREFIX] ?? ''; + $this->middleware = array_merge($this->middleware, $attributes[State::MIDDLEWARE] ?? []); + } + + /** + * @return string + */ + public function getPrefix(): string + { + return $this->prefix; + } + + /** + * @param string $prefix + */ + public function setPrefix(string $prefix): void + { + $this->prefix = $prefix; + } + + /** + * @return array + */ + public function getMiddleware(): array + { + return $this->middleware; + } + + /** + * @param array $middleware + */ + public function setMiddleware(array $middleware): void + { + $this->middleware = $middleware; + } + + /** + * @return string|null + */ + public function getDomain(): ?string + { + return $this->domain; + } + + /** + * @param string|null $domain + */ + public function setDomain(?string $domain): void + { + $this->domain = $domain; + } +} diff --git a/src/Routing/Storekeeper.php b/src/Routing/Storekeeper.php new file mode 100644 index 0000000..eeba652 --- /dev/null +++ b/src/Routing/Storekeeper.php @@ -0,0 +1,80 @@ +store = $store; + $this->state = $state; + } + + /** + * Add a route to the collection + * + * @param string $method + * @param string $path + * @param $controller + * @param string|null $name + */ + public function add(string $method, string $path, $controller, ?string $name = null): void + { + $this->store->save( + $method, + $this->state->getPrefix() . $path, + $controller, + $name, + $this->state->getMiddleware(), + $this->state->getDomain() + ); + } + + /** + * @return Repository + */ + public function getStore(): Repository + { + return $this->store; + } + + /** + * @param Repository $store + */ + public function setStore(Repository $store): void + { + $this->store = $store; + } + + /** + * @return State + */ + public function getState(): State + { + return $this->state; + } + + /** + * @param State $state + */ + public function setState(State $state): void + { + $this->state = $state; + } +} diff --git a/src/Url.php b/src/Url.php index 31696ac..5e3c364 100644 --- a/src/Url.php +++ b/src/Url.php @@ -3,23 +3,23 @@ namespace MiladRahimi\PhpRouter; use MiladRahimi\PhpRouter\Exceptions\UndefinedRouteException; -use MiladRahimi\PhpRouter\Routes\Store; +use MiladRahimi\PhpRouter\Routing\Repository; class Url { /** - * @var Store + * @var Repository */ - private $store; + private $repository; /** * Url constructor. * - * @param Store $store + * @param Repository $repository */ - public function __construct(Store $store) + public function __construct(Repository $repository) { - $this->store = $store; + $this->repository = $repository; } /** @@ -32,7 +32,7 @@ public function __construct(Store $store) */ public function make(string $routeName, array $parameters = []): string { - if (!($route = $this->store->findByName($routeName))) { + if (!($route = $this->repository->findByName($routeName))) { throw new UndefinedRouteException("There is no route named `$routeName`."); } diff --git a/tests/InjectionTest.php b/tests/InjectionTest.php index 9bcca28..1992a09 100644 --- a/tests/InjectionTest.php +++ b/tests/InjectionTest.php @@ -4,7 +4,7 @@ use Laminas\Diactoros\ServerRequest; use MiladRahimi\PhpContainer\Container; -use MiladRahimi\PhpRouter\Routes\Route; +use MiladRahimi\PhpRouter\Routing\Route; use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; use Throwable; diff --git a/tests/NamingTest.php b/tests/NamingTest.php index b50cba9..44a853e 100644 --- a/tests/NamingTest.php +++ b/tests/NamingTest.php @@ -3,7 +3,7 @@ namespace MiladRahimi\PhpRouter\Tests; use MiladRahimi\PhpRouter\Enums\HttpMethods; -use MiladRahimi\PhpRouter\Routes\Route; +use MiladRahimi\PhpRouter\Routing\Route; use Throwable; class NamingTest extends TestCase diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php index 3731249..fe7386e 100644 --- a/tests/RoutingTest.php +++ b/tests/RoutingTest.php @@ -6,7 +6,7 @@ use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Tests\Testing\SampleController; -use MiladRahimi\PhpRouter\Routes\Route; +use MiladRahimi\PhpRouter\Routing\Route; use Psr\Http\Message\ServerRequestInterface; use Throwable; use Laminas\Diactoros\ServerRequest; diff --git a/tests/TestCase.php b/tests/TestCase.php index f46c761..8c08446 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -14,7 +14,7 @@ class TestCase extends BaseTestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { parent::setUp(); From 5c10fff5ec30c27b7339a65f3f724f8b41062ef4 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 9 Jan 2021 18:29:22 +0330 Subject: [PATCH 35/52] update --- composer.json | 88 ++++++++++--------- .../{Testing => Common}/SampleController.php | 2 +- .../{Testing => Common}/SampleMiddleware.php | 2 +- .../{Testing => Common}/StopperMiddleware.php | 2 +- tests/{Testing => Common}/TrapPublisher.php | 2 +- tests/ContainerTest.php | 3 - tests/ControllerTest.php | 2 +- tests/GroupingTest.php | 2 +- tests/HttpMethodTest.php | 2 +- tests/MiddlewareTest.php | 4 +- tests/RoutingTest.php | 6 +- tests/TestCase.php | 2 +- 12 files changed, 59 insertions(+), 58 deletions(-) rename tests/{Testing => Common}/SampleController.php (80%) rename tests/{Testing => Common}/SampleMiddleware.php (93%) rename tests/{Testing => Common}/StopperMiddleware.php (93%) rename tests/{Testing => Common}/TrapPublisher.php (95%) diff --git a/composer.json b/composer.json index 0b9e5f4..3de002b 100644 --- a/composer.json +++ b/composer.json @@ -1,45 +1,49 @@ { - "name": "miladrahimi/phprouter", - "description": "A powerful, lightweight, and very fast HTTP URL router for PHP projects.", - "keywords": [ - "Route", - "Router", - "Routing", - "URL Router", - "HTTP Router", - "URL", - "HTTP" - ], - "homepage": "https://github.com/miladrahimi/phprouter", - "type": "library", - "license": "MIT", - "authors": [ - { - "name": "Milad Rahimi", - "email": "info@miladrahimi.com", - "homepage": "https://miladrahimi.com", - "role": "Developer" - } - ], - "support": { - "email": "info@miladrahimi.com", - "source": "https://github.com/miladrahimi/phprouter/issues" - }, - "require": { - "php": ">=7.1", - "ext-json": "*", - "ext-mbstring": "*", - "laminas/laminas-diactoros": "^2.2", - "miladrahimi/phpcontainer": "^5.0" - }, - "require-dev": { - "phpunit/phpunit": "^7|^8" - }, - "autoload": { - "psr-4": { - "MiladRahimi\\PhpRouter\\": "src/", - "MiladRahimi\\PhpRouter\\Tests\\": "tests/", - "MiladRahimi\\PhpRouter\\Examples\\": "examples/" - } + "name": "miladrahimi/phprouter", + "description": "A powerful, lightweight, and very fast HTTP URL router for PHP projects.", + "keywords": [ + "Route", + "Router", + "Routing", + "URL Router", + "HTTP Router", + "URL", + "HTTP" + ], + "homepage": "https://github.com/miladrahimi/phprouter", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Milad Rahimi", + "email": "info@miladrahimi.com", + "homepage": "https://miladrahimi.com", + "role": "Developer" } + ], + "support": { + "email": "info@miladrahimi.com", + "source": "https://github.com/miladrahimi/phprouter/issues" + }, + "require": { + "php": ">=7.1", + "ext-json": "*", + "ext-mbstring": "*", + "laminas/laminas-diactoros": "^2.2", + "miladrahimi/phpcontainer": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8" + }, + "autoload": { + "psr-4": { + "MiladRahimi\\PhpRouter\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "MiladRahimi\\PhpRouter\\Tests\\": "tests/", + "MiladRahimi\\PhpRouter\\Examples\\": "examples/" + } + } } diff --git a/tests/Testing/SampleController.php b/tests/Common/SampleController.php similarity index 80% rename from tests/Testing/SampleController.php rename to tests/Common/SampleController.php index 8fababd..cd534d4 100644 --- a/tests/Testing/SampleController.php +++ b/tests/Common/SampleController.php @@ -1,6 +1,6 @@ getName(), $r->getPath(), $r->getUri(), - $r->getParameters(), + count($r->getParameters()), $r->getMethod(), count($r->getMiddleware()), $r->getDomain() ?? '-', @@ -155,7 +155,7 @@ public function test_current_route() }, 'home'); $router->dispatch(); - $value = join(',', ['home', '/', '/', [], 'GET', 0, '-']); + $value = join(',', ['home', '/', '/', 0, 'GET', 0, '-']); $this->assertEquals($value, $this->output($router)); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 8c08446..1559c83 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,7 +6,7 @@ use MiladRahimi\PhpContainer\Exceptions\ContainerException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Services\Publisher; -use MiladRahimi\PhpRouter\Tests\Testing\TrapPublisher; +use MiladRahimi\PhpRouter\Tests\Common\TrapPublisher; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase From 31629b445d77424c112e37bb6e5187dcd44c724b Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 9 Jan 2021 19:08:45 +0330 Subject: [PATCH 36/52] update --- .gitignore | 1 + src/Router.php | 64 ---------------- tests/ContainerTest.php | 4 +- tests/RoutingTest.php | 161 ---------------------------------------- tests/TestCase.php | 13 +++- 5 files changed, 13 insertions(+), 230 deletions(-) delete mode 100644 tests/RoutingTest.php diff --git a/.gitignore b/.gitignore index c0df8ed..e145d12 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /composer.lock /build .DS_Store +.phpunit.result.cache diff --git a/src/Router.php b/src/Router.php index 7ab8ea1..9ed5208 100644 --- a/src/Router.php +++ b/src/Router.php @@ -267,14 +267,6 @@ public function options(string $path, $controller, ?string $name = null): void $this->map('OPTIONS', $path, $controller, $name); } - /** - * @param Container $container - */ - public function setContainer(Container $container): void - { - $this->container = $container; - } - /** * @return Container */ @@ -283,54 +275,6 @@ public function getContainer(): Container return $this->container; } - /** - * @return Storekeeper - */ - public function getStorekeeper(): Storekeeper - { - return $this->storekeeper; - } - - /** - * @param Storekeeper $storekeeper - */ - public function setStorekeeper(Storekeeper $storekeeper): void - { - $this->storekeeper = $storekeeper; - } - - /** - * @return Matcher - */ - public function getMatcher(): Matcher - { - return $this->matcher; - } - - /** - * @param Matcher $matcher - */ - public function setMatcher(Matcher $matcher): void - { - $this->matcher = $matcher; - } - - /** - * @return Caller - */ - public function getCaller(): Caller - { - return $this->caller; - } - - /** - * @param Caller $caller - */ - public function setCaller(Caller $caller): void - { - $this->caller = $caller; - } - /** * @return Publisher */ @@ -338,12 +282,4 @@ public function getPublisher(): Publisher { return $this->publisher; } - - /** - * @param Publisher $publisher - */ - public function setPublisher(Publisher $publisher): void - { - $this->publisher = $publisher; - } } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index d0c0480..7cd6a32 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -10,10 +10,10 @@ class ContainerTest extends TestCase /** * @throws Throwable */ - public function test_setting_and_getting_container() + public function test_binding_and_resolving_with_container() { $router = $this->router(); - $router->getContainer()->singleton('name', 'Pink Floyd');; + $router->getContainer()->singleton('name', 'Pink Floyd'); $router->get('/', function (Container $container) { return $container->get('name'); diff --git a/tests/RoutingTest.php b/tests/RoutingTest.php deleted file mode 100644 index cc1d393..0000000 --- a/tests/RoutingTest.php +++ /dev/null @@ -1,161 +0,0 @@ -router(); - $router->get('/', function (ServerRequestInterface $r) { - return $r->getMethod(); - }); - $router->dispatch(); - - $this->assertEquals('GET', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_injection_of_request_by_type() - { - $router = $this->router(); - $router->get('/', function (ServerRequest $r) { - return $r->getMethod(); - }); - $router->dispatch(); - - $this->assertEquals('GET', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_injection_of_default_value() - { - $router = $this->router(); - $router->get('/', function ($default = "Default") { - return $default; - }); - $router->dispatch(); - - $this->assertEquals('Default', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_default_publisher() - { - ob_start(); - - $router = Router::create(); - - $router->get('/', function () { - return 'home'; - }); - $router->dispatch(); - - $this->assertEquals('home', ob_get_contents()); - - ob_end_clean(); - } - - /** - * @throws Throwable - */ - public function test_with_fully_namespaced_controller() - { - $router = $this->router(); - $router->get('/', [SampleController::class, 'home']); - $router->dispatch(); - - $this->assertEquals('Home', $this->output($router)); - } - - /** - * @throws Throwable - */ - public function test_not_found_error() - { - $this->mockRequest('GET', 'http://example.com/unknowon'); - - $this->expectException(RouteNotFoundException::class); - - $router = $this->router(); - $router->get('/', $this->OkController()); - $router->dispatch(); - } - - /** - * @throws Throwable - */ - public function test_with_class_method_but_invalid_controller_class() - { - $this->expectException(InvalidCallableException::class); - - $router = $this->router(); - $router->get('/', 'UnknownController@method'); - $router->dispatch(); - } - - /** - * @throws Throwable - */ - public function test_with_class_but_invalid_method() - { - $this->expectException(InvalidCallableException::class); - - $router = $this->router(); - $router->get('/', SampleController::class . '@invalid'); - $router->dispatch(); - } - - /** - * @throws Throwable - */ - public function test_with_invalid_controller_class() - { - $this->expectException(InvalidCallableException::class); - - $router = $this->router(); - $router->get('/', 666); - $router->dispatch(); - } - - /** - * @throws Throwable - */ - public function test_current_route() - { - $router = $this->router(); - $router->get('/', function (Route $r) { - return join(',', [ - $r->getName(), - $r->getPath(), - $r->getUri(), - count($r->getParameters()), - $r->getMethod(), - count($r->getMiddleware()), - $r->getDomain() ?? '-', - ]); - }, 'home'); - $router->dispatch(); - - $value = join(',', ['home', '/', '/', 0, 'GET', 0, '-']); - $this->assertEquals($value, $this->output($router)); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 1559c83..f4d2f25 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,11 +3,15 @@ namespace MiladRahimi\PhpRouter\Tests; use Closure; +use MiladRahimi\PhpContainer\Container; use MiladRahimi\PhpContainer\Exceptions\ContainerException; use MiladRahimi\PhpRouter\Router; +use MiladRahimi\PhpRouter\Routing\Repository; +use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Common\TrapPublisher; use PHPUnit\Framework\TestCase as BaseTestCase; +use Psr\Container\ContainerInterface; class TestCase extends BaseTestCase { @@ -44,10 +48,13 @@ protected function mockRequest(string $method, string $url): void */ protected function router(): Router { - $router = Router::create(); - $router->setPublisher(new TrapPublisher()); + $container = new Container(); + $container->singleton(Container::class, $container); + $container->singleton(ContainerInterface::class, $container); + $container->singleton(Repository::class, new Repository()); + $container->singleton(Publisher::class, TrapPublisher::class); - return $router; + return $container->instantiate(Router::class); } /** From 8c92f8c638f6c0b6b457923db0d67127f06d7c4e Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 9 Jan 2021 19:15:04 +0330 Subject: [PATCH 37/52] remove testing ok controller closure --- tests/Common/SampleController.php | 8 +++++--- tests/ControllerTest.php | 24 ++++-------------------- tests/GroupingTest.php | 15 ++++++++------- tests/MiddlewareTest.php | 9 +++++---- tests/TestCase.php | 14 -------------- 5 files changed, 22 insertions(+), 48 deletions(-) diff --git a/tests/Common/SampleController.php b/tests/Common/SampleController.php index cd534d4..d30f806 100644 --- a/tests/Common/SampleController.php +++ b/tests/Common/SampleController.php @@ -4,9 +4,6 @@ class SampleController { - /** - * @return string - */ public function home() { return 'Home'; @@ -16,4 +13,9 @@ public function page() { return 'Page'; } + + public function ok() + { + return 'OK'; + } } diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 35bb595..6d89edd 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -11,7 +11,7 @@ class ControllerTest extends TestCase /** * @throws Throwable */ - public function test_a_closure_controller() + public function test_with_a_closure_controller() { $router = $this->router(); $router->get('/', function () { @@ -25,7 +25,7 @@ public function test_a_closure_controller() /** * @throws Throwable */ - public function test_a_class_method_controller() + public function test_with_a_method_controller() { $router = $this->router(); $router->get('/', [SampleController::class, 'home']); @@ -37,23 +37,7 @@ public function test_a_class_method_controller() /** * @throws Throwable */ - public function test_a_function_controller_it_is_deprecated_and_should_fail() - { - function home() { - return 'Function'; - } - - $router = $this->router(); - $router->get('/', 'home'); - - $this->expectException(InvalidCallableException::class); - $router->dispatch(); - } - - /** - * @throws Throwable - */ - public function test_an_invalid_array_as_controller_it_should_fail() + public function test_with_an_invalid_array_as_controller_it_should_fail() { $router = $this->router(); $router->get('/', ['invalid', 'array', 'controller']); @@ -65,7 +49,7 @@ public function test_an_invalid_array_as_controller_it_should_fail() /** * @throws Throwable */ - public function test_multiple_controller_for_the_same_route_it_should_call_the_last_one() + public function test_with_multiple_controller_for_the_same_route_it_should_call_the_last_one() { $router = $this->router(); $router->get('/', [SampleController::class, 'home']); diff --git a/tests/GroupingTest.php b/tests/GroupingTest.php index a14cb18..466d556 100644 --- a/tests/GroupingTest.php +++ b/tests/GroupingTest.php @@ -3,6 +3,7 @@ namespace MiladRahimi\PhpRouter\Tests; use MiladRahimi\PhpRouter\Router; +use MiladRahimi\PhpRouter\Tests\Common\SampleController; use MiladRahimi\PhpRouter\Tests\Common\SampleMiddleware; use Throwable; @@ -15,7 +16,7 @@ public function test_with_no_attribute() { $router = $this->router(); $router->group([], function (Router $router) { - $router->get('/', $this->OkController()); + $router->get('/', [SampleController::class, 'ok']); }); $router->dispatch(); @@ -31,7 +32,7 @@ public function test_with_a_middleware() $router = $this->router(); $router->group(['middleware' => [$middleware]], function (Router $router) { - $router->get('/', $this->OkController()); + $router->get('/', [SampleController::class, 'ok']); }); $router->dispatch(); @@ -50,7 +51,7 @@ public function test_nested_groups_with_middleware() $router = $this->router(); $router->group(['middleware' => [$group1Middleware]], function (Router $router) use ($group2Middleware) { $router->group(['middleware' => [$group2Middleware]], function (Router $router) { - $router->get('/', $this->OkController()); + $router->get('/', [SampleController::class, 'ok']); }); }); $router->dispatch(); @@ -69,7 +70,7 @@ public function test_with_a_prefix() $router = $this->router(); $router->group(['prefix' => '/group'], function (Router $router) { - $router->get('/page', $this->OkController()); + $router->get('/page', [SampleController::class, 'ok']); }); $router->dispatch(); @@ -86,7 +87,7 @@ public function test_nested_groups_with_prefix() $router = $this->router(); $router->group(['prefix' => '/group1'], function (Router $router) { $router->group(['prefix' => '/group2'], function (Router $router) { - $router->get('/page', $this->OkController()); + $router->get('/page', [SampleController::class, 'ok']); }); }); $router->dispatch(); @@ -103,7 +104,7 @@ public function test_with_domain() $router = $this->router(); $router->group(['domain' => 'sub.domain.tld'], function (Router $router) { - $router->get('/', $this->OkController()); + $router->get('/', [SampleController::class, 'ok']); }); $router->dispatch(); @@ -120,7 +121,7 @@ public function test_nested_groups_with_domain_it_should_consider_the_inner_grou $router = $this->router(); $router->group(['domain' => 'sub1.domain.com'], function (Router $router) { $router->group(['domain' => 'sub2.domain.com'], function (Router $router) { - $router->get('/', $this->OkController()); + $router->get('/', [SampleController::class, 'ok']); }); }); $router->dispatch(); diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index d90b37c..9bfb388 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -4,6 +4,7 @@ use MiladRahimi\PhpRouter\Exceptions\InvalidCallableException; use MiladRahimi\PhpRouter\Router; +use MiladRahimi\PhpRouter\Tests\Common\SampleController; use MiladRahimi\PhpRouter\Tests\Common\SampleMiddleware; use MiladRahimi\PhpRouter\Tests\Common\StopperMiddleware; use Throwable; @@ -19,7 +20,7 @@ public function test_with_a_single_middleware_as_an_object() $router = $this->router(); $router->group(['middleware' => [$middleware]], function (Router $r) { - $r->get('/', $this->OkController()); + $r->get('/', [SampleController::class, 'ok']); }); $router->dispatch(); @@ -36,7 +37,7 @@ public function test_with_a_single_middleware_as_a_string() $router = $this->router(); $router->group(['middleware' => [$middleware]], function (Router $r) { - $r->get('/', $this->OkController()); + $r->get('/', [SampleController::class, 'ok']); }); $router->dispatch(); @@ -53,7 +54,7 @@ public function test_with_a_stopper_middleware() $router = $this->router(); $router->group(['middleware' => [$middleware]], function (Router $r) { - $r->get('/', $this->OkController()); + $r->get('/', [SampleController::class, 'ok']); }); $router->dispatch(); @@ -70,7 +71,7 @@ public function test_with_invalid_middleware() $router = $this->router(); $router->group(['middleware' => ['UnknownMiddleware']], function (Router $r) { - $r->get('/', $this->OkController()); + $r->get('/', [SampleController::class, 'ok']); }); $router->dispatch(); } diff --git a/tests/TestCase.php b/tests/TestCase.php index f4d2f25..9405036 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,12 +2,10 @@ namespace MiladRahimi\PhpRouter\Tests; -use Closure; use MiladRahimi\PhpContainer\Container; use MiladRahimi\PhpContainer\Exceptions\ContainerException; use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Routing\Repository; -use MiladRahimi\PhpRouter\Services\HttpPublisher; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Common\TrapPublisher; use PHPUnit\Framework\TestCase as BaseTestCase; @@ -57,18 +55,6 @@ protected function router(): Router return $container->instantiate(Router::class); } - /** - * Get a sample controller that returns an 'OK' string - * - * @return Closure - */ - protected function OkController(): Closure - { - return function () { - return 'OK'; - }; - } - /** * Get the generated output of the dispatched route of the given router * From 4bb53b52d9375b2a575bb45ecd7cf3c4b54a558c Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 9 Jan 2021 19:21:52 +0330 Subject: [PATCH 38/52] update controller tests --- tests/Common/SampleMiddleware.php | 3 --- tests/ControllerTest.php | 12 ++++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/Common/SampleMiddleware.php b/tests/Common/SampleMiddleware.php index 696e37f..59eb341 100644 --- a/tests/Common/SampleMiddleware.php +++ b/tests/Common/SampleMiddleware.php @@ -28,9 +28,6 @@ public function __construct(string $content = null) $this->content = $content ?: 'empty'; } - /** - * @inheritdoc - */ public function handle(ServerRequestInterface $request, $next) { static::$output[] = $this->content; diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 6d89edd..28eebe3 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -46,6 +46,18 @@ public function test_with_an_invalid_array_as_controller_it_should_fail() $router->dispatch(); } + /** + * @throws Throwable + */ + public function test_with_an_invalid_method_as_controller_it_should_fail() + { + $router = $this->router(); + $router->get('/', [SampleController::class, 'invalid']); + + $this->expectException(InvalidCallableException::class); + $router->dispatch(); + } + /** * @throws Throwable */ From 01253f6bfd9d7a8a7bd35c01d1fede0b70d9a184 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 9 Jan 2021 19:24:17 +0330 Subject: [PATCH 39/52] update naming tests --- tests/NamingTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/NamingTest.php b/tests/NamingTest.php index 44a853e..a4a2653 100644 --- a/tests/NamingTest.php +++ b/tests/NamingTest.php @@ -2,7 +2,6 @@ namespace MiladRahimi\PhpRouter\Tests; -use MiladRahimi\PhpRouter\Enums\HttpMethods; use MiladRahimi\PhpRouter\Routing\Route; use Throwable; From c3d4117e9b8da194fda3f6420ac31ca99fe7d374 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 11:07:33 +0330 Subject: [PATCH 40/52] add setPublisher --- README.md | 16 ++++++++-------- src/Router.php | 8 ++++++++ tests/TestCase.php | 9 +++------ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9d89a17..145422a 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,17 @@ PhpRouter is a powerful, lightweight, and very fast HTTP URL router for PHP proj Some of the provided features: * Route parameters * Middleware -* Route groups (URI prefix, namespace prefix, middleware, and domain) -* Route names +* Route groups (by prefix, middleware, and domain) +* Route naming * PSR-7 requests and responses -* View module (A simple view layer) -* Multiple domains or subdomains (regex pattern) -* Multiple controller types (class, closure, and function) -* Predefined route parameter regex patterns +* Multiple (sub)domains (using regex patterns) +* Closure and class controllers +* Predefined route parameter patterns * Custom HTTP methods -* Request, response and router instance injection +* Integrated with IoC container out of the box +* Auto-injection of request, response, router, etc -Current version requires PHP `v7.1` or newer versions. +The current version requires PHP `v7.1` or newer versions. ## Contents - [PhpRouter](#phprouter) diff --git a/src/Router.php b/src/Router.php index 9ed5208..9726a94 100644 --- a/src/Router.php +++ b/src/Router.php @@ -282,4 +282,12 @@ public function getPublisher(): Publisher { return $this->publisher; } + + /** + * @param Publisher $publisher + */ + public function setPublisher(Publisher $publisher): void + { + $this->publisher = $publisher; + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 9405036..eb08c02 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -46,13 +46,10 @@ protected function mockRequest(string $method, string $url): void */ protected function router(): Router { - $container = new Container(); - $container->singleton(Container::class, $container); - $container->singleton(ContainerInterface::class, $container); - $container->singleton(Repository::class, new Repository()); - $container->singleton(Publisher::class, TrapPublisher::class); + $router = Router::create(); + $router->setPublisher(new TrapPublisher()); - return $container->instantiate(Router::class); + return $router; } /** From 1b582db29f3e27691ed534a5ed117576eec78ccb Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 11:38:44 +0330 Subject: [PATCH 41/52] remove options http method --- src/Router.php | 12 ------------ tests/HttpMethodTest.php | 14 -------------- tests/TestCase.php | 3 --- 3 files changed, 29 deletions(-) diff --git a/src/Router.php b/src/Router.php index 9726a94..99e34e5 100644 --- a/src/Router.php +++ b/src/Router.php @@ -255,18 +255,6 @@ public function delete(string $path, $controller, ?string $name = null): void $this->map('DELETE', $path, $controller, $name); } - /** - * Map a controller to given OPTIONS route - * - * @param string $path - * @param Closure|callable|string $controller - * @param string|null $name - */ - public function options(string $path, $controller, ?string $name = null): void - { - $this->map('OPTIONS', $path, $controller, $name); - } - /** * @return Container */ diff --git a/tests/HttpMethodTest.php b/tests/HttpMethodTest.php index 31642c4..41a9692 100644 --- a/tests/HttpMethodTest.php +++ b/tests/HttpMethodTest.php @@ -79,20 +79,6 @@ public function test_a_delete_route() $this->assertEquals('Home', $this->output($router)); } - /** - * @throws Throwable - */ - public function test_an_options_route() - { - $this->mockRequest('OPTIONS', 'http://example.com/'); - - $router = $this->router(); - $router->options('/', [SampleController::class, 'home']); - $router->dispatch(); - - $this->assertEquals('Home', $this->output($router)); - } - /** * @throws Throwable */ diff --git a/tests/TestCase.php b/tests/TestCase.php index eb08c02..913aeda 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,14 +2,11 @@ namespace MiladRahimi\PhpRouter\Tests; -use MiladRahimi\PhpContainer\Container; use MiladRahimi\PhpContainer\Exceptions\ContainerException; use MiladRahimi\PhpRouter\Router; -use MiladRahimi\PhpRouter\Routing\Repository; use MiladRahimi\PhpRouter\Services\Publisher; use MiladRahimi\PhpRouter\Tests\Common\TrapPublisher; use PHPUnit\Framework\TestCase as BaseTestCase; -use Psr\Container\ContainerInterface; class TestCase extends BaseTestCase { From 4b58bfb555992d4629a4915c673bb84dfc89f825 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 15:54:10 +0330 Subject: [PATCH 42/52] update README --- README.md | 529 +++++++++++++------------- examples/{example-01 => 01}/index.php | 4 +- examples/02/index.php | 25 ++ examples/03/index.php | 19 + examples/04/index.php | 13 + examples/{example-04 => 05}/index.php | 2 +- examples/06/index.php | 13 + examples/07/index.php | 28 ++ examples/{example-09 => 08}/index.php | 12 +- examples/{example-10 => 09}/index.php | 3 +- examples/10/index.php | 23 ++ examples/11/index.php | 33 ++ examples/{example-16 => 12}/index.php | 18 +- examples/{example-14 => 13}/index.php | 16 +- examples/14/index.php | 30 ++ examples/15/index.php | 22 ++ examples/16/index.php | 24 ++ examples/{example-20 => 17}/index.php | 2 +- examples/Shared/PostModel.php | 14 - examples/Shared/SimpleMiddleware.php | 6 +- examples/example-02/index.php | 25 -- examples/example-03/index.php | 19 - examples/example-05/index.php | 18 - examples/example-06/index.php | 20 - examples/example-07/index.php | 14 - examples/example-08/index.php | 12 - examples/example-11/index.php | 34 -- examples/example-12/index.php | 30 -- examples/example-13/index.php | 14 - examples/example-15/index.php | 18 - examples/example-17/index.php | 19 - examples/example-18/index.php | 26 -- examples/example-19/index.php | 30 -- src/Router.php | 8 +- 34 files changed, 524 insertions(+), 599 deletions(-) rename examples/{example-01 => 01}/index.php (68%) create mode 100644 examples/02/index.php create mode 100644 examples/03/index.php create mode 100644 examples/04/index.php rename examples/{example-04 => 05}/index.php (87%) create mode 100644 examples/06/index.php create mode 100644 examples/07/index.php rename examples/{example-09 => 08}/index.php (79%) rename examples/{example-10 => 09}/index.php (81%) create mode 100644 examples/10/index.php create mode 100644 examples/11/index.php rename examples/{example-16 => 12}/index.php (58%) rename examples/{example-14 => 13}/index.php (60%) create mode 100644 examples/14/index.php create mode 100644 examples/15/index.php create mode 100644 examples/16/index.php rename examples/{example-20 => 17}/index.php (94%) delete mode 100644 examples/Shared/PostModel.php delete mode 100644 examples/example-02/index.php delete mode 100644 examples/example-03/index.php delete mode 100644 examples/example-05/index.php delete mode 100644 examples/example-06/index.php delete mode 100644 examples/example-07/index.php delete mode 100644 examples/example-08/index.php delete mode 100644 examples/example-11/index.php delete mode 100644 examples/example-12/index.php delete mode 100644 examples/example-13/index.php delete mode 100644 examples/example-15/index.php delete mode 100644 examples/example-17/index.php delete mode 100644 examples/example-18/index.php delete mode 100644 examples/example-19/index.php diff --git a/README.md b/README.md index 145422a..121abea 100644 --- a/README.md +++ b/README.md @@ -24,34 +24,42 @@ Some of the provided features: The current version requires PHP `v7.1` or newer versions. ## Contents -- [PhpRouter](#phprouter) - - [Contents](#contents) - - [Versions](#versions) - - [Installation](#installation) - - [Configuration](#configuration) - - [Documentation](#documentation) +- [Versions](#versions) +- [Documentation](#documentation) + - [Installation](#installation) + - [Configuration](#configuration) - [Getting Started](#getting-started) - [HTTP Methods](#http-methods) - [Controllers](#controllers) - [Route Parameters](#route-parameters) - - [HTTP Request and Request](#http-request-and-request) - - [Middleware](#middleware) - - [Domain and Sub-domain](#domain-and-sub-domain) + - [Requests and Responses](#requests-and-responses) - [Route Groups](#route-groups) - - [Route Name](#route-name) + - [Middleware](#middleware) + - [Domains and Subdomains](#domains-and-subdomains) + - [Route Names](#route-names) - [Current Route](#current-route) - [Error Handling](#error-handling) - - [License](#license) +- [License](#license) ## Versions -* **v5.x.x (LTS)** -* **v4.x.x (LTS)** -* v3.x.x (Unsupported) -* ~~v2.x.x~~ (Unavailable) -* ~~v1.x.x~~ (Unavailable) +Supported versions: + +* v5.x.x +* v4.x.x + +Unsupported versions: + +* v3.x.x -## Installation +Unavailable versions: + +* v2.x.x +* v1.x.x + +## Documentation + +### Installation Install [Composer](https://getcomposer.org) and run following command in your project's root directory: @@ -59,9 +67,18 @@ Install [Composer](https://getcomposer.org) and run following command in your pr composer require miladrahimi/phprouter "5.*" ``` -## Configuration +### Configuration -First of all, you need to configure your webserver to handle all the HTTP requests with a single PHP file like `index.php`. Here you can see sample configurations for Apache HTTP Server and NGINX. +First of all, +you need to configure your webserver to handle all the HTTP requests with a single PHP file like the `index.php` file. +Here you can see sample configurations for NGINX and Apache HTTP Server. + +* NGINX configuration sample: + ```nginx + location / { + try_files $uri $uri/ /index.php?$query_string; + } + ``` * Apache `.htaccess` sample: ```apacheconfig @@ -81,23 +98,14 @@ First of all, you need to configure your webserver to handle all the HTTP reques ``` -* NGINX configuration sample: - ```nginx - location / { - try_files $uri $uri/ /index.php?$query_string; - } - ``` - -## Documentation - ### Getting Started -It's so easy to work with PhpRouter! Just take a look at this example: +It's so easy to work with PhpRouter! Just take a look at the following example. ```php use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); $router->get('/', function () { return 'This is homepage!'; @@ -106,171 +114,154 @@ $router->get('/', function () { $router->dispatch(); ``` -And if you are a fan of method chaining: +### HTTP Methods + +The following example illustrates how to declare different routes for different HTTP methods. ```php use MiladRahimi\PhpRouter\Router; -$router = new Router(); - -$router - ->get('/', function () { - return '

This is homepage!

'; - }) - ->dispatch(); -``` +$router = Router::create(); -There are more examples available [here](https://github.com/miladrahimi/phprouter/tree/master/examples). +$router->get('/', function () { + return 'GET'; +}); +$router->post('/', function () { + return 'POST'; +}); +$router->put('/', function () { + return 'PUT'; +}); +$router->patch('/', function () { + return 'PATCH'; +}); +$router->delete('/', function () { + return 'DELETE'; +}); -### HTTP Methods +$router->dispatch(); +``` -The following example illustrates how to declare different routes for different HTTP methods. There is also `any` method for handling a route with any HTTP method. +You can use the `map()` method for other HTTP methods like this example: ```php use MiladRahimi\PhpRouter\Router; -$router = new Router(); - -$router - ->get('/', function () { - return 'GET method'; - }) - ->post('/', function () { - return 'POST method'; - }) - ->patch('/', function () { - return 'PATCH method'; - }) - ->put('/', function () { - return 'PUT method'; - }) - ->delete('/', function () { - return 'DELETE method'; - }) - ->any('/page', function () { - return 'This is the Page! No matter what the HTTP method is!'; - }) - ->dispatch(); -``` - -Or you may want to use your custom HTTP methods, so take a look at this example: +$router = Router::create(); -```php -use MiladRahimi\PhpRouter\Router; +$router->map('GET', '/', function () { + return 'GET'; +}); +$router->map('OPTIONS', '/', function () { + return 'OPTIONS'; +}); +$router->map('CUSTOM', '/', function () { + return 'CUSTOM'; +}); -$router = new Router(); - -$router - ->map('GET', '/', function () { - return 'GET method'; - }) - ->map('POST', '/', function () { - return 'POST method'; - }) - ->map('CUSTOM', '/', function () { - return 'CUSTOM method'; - }) - ->dispatch(); +$router->dispatch(); ``` -### Controllers - -PhpRouter supports plenty of controller types, just look at the following examples. +If you need to assign multiple HTTP methods to a single controller, there is the `match()` method for you. ```php use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); -$router->get('/closure', function () { - return 'Closure as a controller'; +$router->match(['GET', 'POST'], '/', function () { + return 'GET or POST!'; }); -function func() { - return 'Function as a controller'; -} -$router->get('/function', 'func'); - $router->dispatch(); ``` -And here is a controller class: +If you don't want to care about HTTP verbs, you can use the `any()` method. ```php use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); -class Controller -{ - function method() - { - return 'Class method as a controller'; - } -} - -$router->get('/', 'Controller@method'); +$router->any('/', function () { + return 'This is Home! No matter what the HTTP method is!'; +}); $router->dispatch(); ``` -When your controller class has a namespace: +### Controllers + +#### Closure Controllers ```php -use App\Controllers\TheController; use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); -$router->get('/', 'App\Controllers\TheController@show'); -// OR -$router->get('/', TheController::class . '@show'); +$router->get('/', function () { + return 'This is a closure controller!'; +}); $router->dispatch(); ``` -If your controllers have the same namespace or namespace prefix, you can pass it to the router constructor like this: +#### Class Method Controllers ```php use MiladRahimi\PhpRouter\Router; -$router = new Router('', 'App\Controllers'); +class UsersController +{ + function index() + { + return 'Class: UsersController, Method: index'; + } + + function handle() + { + return 'Class UsersController.'; + } +} + +$router = Router::create(); -$router->get('/', 'TheController@show'); -// PhpRouter looks for App\Controllers\TheController@show +// Controller: Class=UsersController Method=index() +$router->get('/method', [UsersController::class, 'index']); + +// Controller: Class=UsersController Method=handle() +$router->get('/class', UsersController::class); $router->dispatch(); ``` ### Route Parameters -A URL might have one or more variable parts like the *id* in a blog post URL. We call it a route parameter. You can catch them by controller arguments with the same names. +A URL might have one or more variable parts like product IDs on a shopping website. +We call it a route parameter. +You can catch them by controller arguments like the example below. ```php use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); // Required parameter $router->get('/post/{id}', function ($id) { return "The content of post $id"; }); - // Optional parameter $router->get('/welcome/{name?}', function ($name = null) { return 'Welcome ' . ($name ?: 'Dear User'); }); - -// Optional parameter, Optional Slash! +// Optional parameter, Optional / (Slash)! $router->get('/profile/?{user?}', function ($user = null) { return ($user ?: 'Your') . ' profile'; }); - // Optional parameter with default value -$router->get('/role/{role?}', function ($role = 'admin') { - return "Role is $role"; +$router->get('/roles/{role?}', function ($role = 'guest') { + return "Your role is $role"; }); - // Multiple parameters $router->get('/post/{pid}/comment/{cid}', function ($pid, $cid) { return "The comment $cid of the post $pid"; @@ -279,141 +270,159 @@ $router->get('/post/{pid}/comment/{cid}', function ($pid, $cid) { $router->dispatch(); ``` -In default, route parameters can hold any value, but you can define regex patterns for each of them. +In default, route parameters can have any value, but you can define regex patterns to limit them. ```php use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); +// "id" must be numeric $router->pattern('id', '[0-9]+'); -$router->get('/blog/post/{id}', function (int $id) { +$router->get('/post/{id}', function (int $id) { return 'Content of the post: ' . $id; }); $router->dispatch(); ``` -### HTTP Request and Request +### Requests and Responses -PhpRouter uses [laminas-diactoros](https://github.com/laminas/laminas-diactoros/) (formerly known as [zend-diactoros](https://github.com/zendframework/zend-diactoros)) package (v2) to provide [PSR-7](https://www.php-fig.org/psr/psr-7) request and response objects to your controllers and middleware. +PhpRouter uses [laminas-diactoros](https://github.com/laminas/laminas-diactoros/) +(formerly known as [zend-diactoros](https://github.com/zendframework/zend-diactoros)) +package (v2) to provide [PSR-7](https://www.php-fig.org/psr/psr-7) +request and response objects to your controllers and middleware. -#### Request +#### Requests -You can catch the PSR-7 request object in your controllers like this example: +You can catch the request object in your controllers like this example: ```php use MiladRahimi\PhpRouter\Router; use Laminas\Diactoros\ServerRequest; -use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse; -$router = new Router(); +$router = Router::create(); -$router->get('/', function (ServerRequest $request) { +$router->get('/test', function (ServerRequest $request) { return new JsonResponse([ 'method' => $request->getMethod(), - 'uri' => $request->getUri(), - 'body' => $request->getBody(), + 'uri' => $request->getUri()->getPath(), + 'body' => $request->getBody()->getContents(), 'parsedBody' => $request->getParsedBody(), 'headers' => $request->getHeaders(), - 'queryStrings' => $request->getQueryParams(), + 'queryParameters' => $request->getQueryParams(), 'attributes' => $request->getAttributes(), ]); }); -$router->post('/posts', function (ServerRequest $request) { - $post = new PostModel(); - $post->title = $request->getQueryParams()['title']; - $post->content = $request->getQueryParams()['content']; - $post->save(); - - return new EmptyResponse(); -}); - $router->dispatch(); ``` -#### Response +#### Responses The example below illustrates the built-in responses. ```php +use Laminas\Diactoros\Response\RedirectResponse; use MiladRahimi\PhpRouter\Router; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\HtmlResponse; use Laminas\Diactoros\Response\JsonResponse; use Laminas\Diactoros\Response\TextResponse; -$router = new Router(); +$router = Router::create(); -$router - ->get('/html/1', function () { - return 'This is an HTML response'; - }) - ->get('/html/2', function () { - return new HtmlResponse('This is also an HTML response', 200); - }) - ->get('/json', function () { - return new JsonResponse(['error' => 'Unauthorized!'], 401); - }) - ->get('/text', function () { - return new TextResponse('This is a plain text...'); - }) - ->get('/empty', function () { - return new EmptyResponse(); // HTTP Status: 204 - }) - ->get('/redirect', function () { - return new RedirectResponse('https://miladrahimi.com'); +$router->get('/html/1', function () { + return 'This is an HTML response'; +}); +$router->get('/html/2', function () { + return new HtmlResponse('This is also an HTML response', 200); +}); +$router->get('/json', function () { + return new JsonResponse(['error' => 'Unauthorized!'], 401); +}); +$router->get('/text', function () { + return new TextResponse('This is a plain text...'); +}); +$router->get('/empty', function () { + return new EmptyResponse(204); +}); +$router->get('/redirect', function () { + return new RedirectResponse('https://miladrahimi.com'); +}); + +$router->dispatch(); +``` + +### Route Groups + +You can categorize routes into groups. +The groups can have common attributes like middleware, domain, or prefix. +The following example shows how to group routes: + +```php +use MiladRahimi\PhpRouter\Router; + +$router = Router::create(); + +// A group with uri prefix +$router->group(['prefix' => '/admin'], function (Router $router) { + // URI: /admin/setting + $router->get('/setting', function () { + return 'Setting Panel'; }); +}); + +// All of group attributes together! +$attributes = [ + 'prefix' => '/admin', + 'domain' => 'shop.example.com', + 'middleware' => [AuthMiddleware::class], +]; + +$router->group($attributes, function (Router $router) { + // URL: http://shop.example.com/admin/users + // Domain: shop.example.com + // Middleware: AuthMiddleware + $router->get('/users', [UsersController::class, 'index']); +}); $router->dispatch(); ``` +The group attributes will be explained later in this documentation. + ### Middleware -PhpRouter supports middleware. You can use it for different purposes such as authentication, authorization, throttles and so forth. Middleware runs before controllers and it can check and manipulate the request and response. +PhpRouter supports middleware. +You can use it for different purposes, such as authentication, authorization, throttles, and so forth. +Middleware runs before controllers, and it can check and manipulate requests and responses. Here you can see the request lifecycle considering some middleware: ``` -[Request] ↦ Router ↦ Middleware 1 ↦ ... ↦ Middleware N ↦ Controller - ↧ - ↤ Router ↤ Middleware 1 ↤ ... ↤ Middleware N ↤ [Response] -``` - -To declare a middleware, you must implement the `Middleware` interface. Here is the Middleware interface: - -```php -interface Middleware -{ - /** - * Handle request and response - * - * @param ServerRequestInterface $request - * @param Closure $next - * @return ResponseInterface|mixed|null - */ - public function handle(ServerRequestInterface $request, Closure $next); -} +[Request] ↦ Router ↦ Middleware 1 ↦ ... ↦ Middleware N ↦ Controller + ↧ +[Response] ↤ Router ↤ Middleware 1 ↤ ... ↤ Middleware N ↤ [Response] ``` -As you can see, a middleware must have a `handle()` method that catches the request and a Closure (which is responsible for running the next middleware or the controller). It must return a response, as well. A middleware can break the lifecycle and return a response or it can run the `$next` closure to continue the lifecycle. - -See the following example. In the implemented middelware, if there is an `Authorization` header in the request, it passes the request to the next middleware or the controller (if there is no more middleware left) and if the header is absent, it returns a JSON response with `401 Authorization Failed ` HTTP status code. +To declare a middleware, you can use closures and classes just like controllers. +To use the middleware, you must group the routes and mention the middleware in the group attributes. +Caution! The middleware attribute in groups takes an array of middleware, not a single one. ```php use MiladRahimi\PhpRouter\Router; -use MiladRahimi\PhpRouter\Middleware; use Psr\Http\Message\ServerRequestInterface; use Laminas\Diactoros\Response\JsonResponse; -class AuthMiddleware implements Middleware +class AuthMiddleware { public function handle(ServerRequestInterface $request, Closure $next) { if ($request->getHeader('Authorization')) { + // Check the auth header... return $next($request); } @@ -421,98 +430,78 @@ class AuthMiddleware implements Middleware } } -$router = new Router(); +$router = Router::create(); -$router->get('/admin', function () { - return 'This is admin panel!'; -}, AuthMiddleware::class); +// The middleware attribute takes an array of middleware, not a single one! +$router->group(['middleware' => [AuthMiddleware::class]], function(Router $router) { + $router->get('/admin', function () { + return 'Admin Panel'; + }); +}); $router->dispatch(); ``` -Middleware can be implemented using closures but it doesn’t make scense to do so! - -### Domain and Sub-domain +As you can see, the middleware catches the request and a closure. +The closure calls the next middleware or the controller if no middleware is left. +The middleware must return a response, as well. +A middleware can break the lifecycle and return a response itself, +or it can call the `$next` closure to continue the lifecycle. -Your application may serve different services on different domains or subdomains. In this case, you can specify the domain or subdomain for your routes. See this example: - -```php -$router = new Router(); - -// Domain -$router->get('/', 'Controller@method', [], 'domain2.com'); - -// Sub-domain -$router->get('/', 'Controller@method', [], 'server2.domain.com'); - -// Sub-domain with regex pattern -$router->get('/', 'Controller@method', [], '(.*).domain.com'); - -$router->dispatch(); -``` - -### Route Groups +### Domains and Subdomains -Application routes can be categorized into groups if they have common attributes like middleware, domain, or prefix. The following example shows how to group routes: +Your application may serve different services on different domains or subdomains. +In this case, you can specify the domain or subdomain for your routes. +See this example: ```php -use MiladRahimi\PhpRouter\Examples\Samples\SimpleMiddleware; use MiladRahimi\PhpRouter\Router; -use MiladRahimi\PhpRouter\Enums\GroupAttributes; -$router = new Router(); +$router = Router::create(); -// A group with uri prefix -$router->group(['prefix' => '/admin'], function (Router $router) { - // URI: /admin/setting - $router->get('/setting', function () { - return 'Setting.'; +// Domain +$router->group(['domain' => 'shop.com'], function(Router $router) { + $router->get('/', function () { + return 'This is shop.com'; }); }); -// All of group properties together! -$attributes = [ - 'prefix' => '/products', - 'namespace' => 'App\Controllers', - 'domain' => 'shop.example.com', - 'middleware' => SimpleMiddleware::class, -]; +// Subdomain +$router->group(['domain' => 'admin.shop.com'], function(Router $router) { + $router->get('/', function () { + return 'This is admin.shop.com'; + }); +}); -// A group with many common properties! -$router->group($attributes, function (Router $router) { - // URI: http://shop.example.com/products/{id} - // Controller: App\Controllers\ShopController@getProduct - // Domain: shop.example.com - // Middleware: SampleMiddleware - $router->get('/{id}', 'ShopController@getProduct'); +// Subdomain with regex pattern +$router->group(['domain' => '(.*).example.com'], function(Router $router) { + $router->get('/', function () { + return 'This is a subdomain'; + }); }); $router->dispatch(); ``` -### Route Name +### Route Names -You can define names for your routes and use them in your codes instead of the URLs. See this example: +You can assign names to your routes and use them in your codes instead of the hard-coded URLs. +See this example: ```php use MiladRahimi\PhpRouter\Router; use Laminas\Diactoros\Response\JsonResponse; +use MiladRahimi\PhpRouter\Url; -$router = new Router(); +$router = Router::create(); -$router->name('about')->get('/about', function () { - return 'About.'; -}); -$router->name('post')->get('/post/{id}', function ($id) { - return 'Content of the post: ' . $id; -}); -$router->name('home')->get('/', function (Router $router) { +$router->get('/', [HomeController::class, 'show'], 'home'); +$router->get('/post/{id}', [PostController::class, 'show'], 'post'); +$router->get('/links', function (Url $url) { return new JsonResponse([ - 'links' => [ - 'about' => $router->url('about'), /* Result: /about */ - 'post1' => $router->url('post', ['id' => 1]), /* Result: /post/1 */ - 'post2' => $router->url('post', ['id' => 2]) /* Result: /post/2 */ - ] + 'about' => $url->make('home'), /* Result: /about */ + 'post1' => $url->make('post', ['id' => 1]), /* Result: /post/1 */ + 'post2' => $url->make('post', ['id' => 2]) /* Result: /post/2 */ ]); }); @@ -521,36 +510,44 @@ $router->dispatch(); ### Current Route -You might want to get information about the current route in your controller. This example shows how to get this information. +You might need to get information about the current route in your controller or middleware. +This example shows how to get this information. ```php use MiladRahimi\PhpRouter\Router; use Laminas\Diactoros\Response\JsonResponse; +use MiladRahimi\PhpRouter\Routing\Route; -$router = new Router(); +$router = Router::create(); -$router->name('home')->get('/', function (Router $router) { +$router->get('/{id}', function (Route $route) { return new JsonResponse([ - 'current_page_name' => $router->currentRoute()->getName(), /* Result: home */ - 'current_page_uri' => $router->currentRoute()->getPath(), /* Result: / */ - 'current_page_method' => $router->currentRoute()->getMethod(), /* Result: GET */ - 'current_page_domain' => $router->currentRoute()->getDomain(), /* Result: null */ + 'uri' => $route->getUri(), /* Result: "/1" */ + 'name' => $route->getName(), /* Result: sample */ + 'path' => $route->getPath(), /* Result: "/{id}" */ + 'method' => $route->getMethod(), /* Result: GET */ + 'domain' => $route->getDomain(), /* Result: null */ + 'parameters' => $route->getParameters(), /* Result: {"id": "1"} */ + 'middleware' => $route->getMiddleware(), /* Result: [] */ + 'controller' => $route->getController(), /* Result: {} */ ]); -}); +}, 'sample'); $router->dispatch(); ``` ### Error Handling -Your application runs through the `Router::dispatch()` method. You should put it in a `try` block and catch exceptions that will be thrown by your application and PhpRouter. +Your application runs through the `Router::dispatch()` method. +You should put it in a `try` block and catch exceptions. +It throws your application and PhpRouter exceptions. ```php use MiladRahimi\PhpRouter\Router; use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use Laminas\Diactoros\Response\HtmlResponse; -$router = new Router(); +$router = Router::create(); $router->get('/', function () { return 'Home.'; @@ -569,14 +566,10 @@ try { PhpRouter throws the following exceptions: * `RouteNotFoundException` if PhpRouter cannot find any route that matches the user request. -* `InvalidControllerException` if PhpRouter cannot invoke the controller. -* `InvalidMiddlewareException` if PhpRouter cannot invoke the middleware. -* `UndefinedRouteException` if `Router::url()` cannot find any route with the given name. +* `InvalidCallableException` if PhpRouter cannot invoke the controller or middleware. The `RouteNotFoundException` should be considered `404 Not found` error. -The `InvalidControllerException` and `InvalidMiddlewareException` exceptions should never be thrown normally, so they should be considered `500 Internal Error`. - ## License PhpRouter is initially created by [Milad Rahimi](https://miladrahimi.com) and released under the [MIT License](http://opensource.org/licenses/mit-license.php). diff --git a/examples/example-01/index.php b/examples/01/index.php similarity index 68% rename from examples/example-01/index.php rename to examples/01/index.php index 90a6f87..34fc123 100644 --- a/examples/example-01/index.php +++ b/examples/01/index.php @@ -4,10 +4,10 @@ use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); $router->get('/', function () { - return '

This is homepage!

'; + return 'This is homepage!'; }); $router->dispatch(); diff --git a/examples/02/index.php b/examples/02/index.php new file mode 100644 index 0000000..982ed21 --- /dev/null +++ b/examples/02/index.php @@ -0,0 +1,25 @@ +get('/', function () { + return 'GET'; +}); +$router->post('/', function () { + return 'POST'; +}); +$router->put('/', function () { + return 'PUT'; +}); +$router->patch('/', function () { + return 'PATCH'; +}); +$router->delete('/', function () { + return 'DELETE'; +}); + +$router->dispatch(); diff --git a/examples/03/index.php b/examples/03/index.php new file mode 100644 index 0000000..74dbea7 --- /dev/null +++ b/examples/03/index.php @@ -0,0 +1,19 @@ +map('GET', '/', function () { + return 'GET'; +}); +$router->map('OPTIONS', '/', function () { + return 'OPTIONS'; +}); +$router->map('CUSTOM', '/', function () { + return 'CUSTOM'; +}); + +$router->dispatch(); diff --git a/examples/04/index.php b/examples/04/index.php new file mode 100644 index 0000000..1272872 --- /dev/null +++ b/examples/04/index.php @@ -0,0 +1,13 @@ +match(['GET', 'POST'], '/', function () { + return 'GET or POST!'; +}); + +$router->dispatch(); diff --git a/examples/example-04/index.php b/examples/05/index.php similarity index 87% rename from examples/example-04/index.php rename to examples/05/index.php index 58e7c3b..2803abb 100644 --- a/examples/example-04/index.php +++ b/examples/05/index.php @@ -4,7 +4,7 @@ use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); $router->any('/', function () { return 'This is Home! No matter what the HTTP method is!'; diff --git a/examples/06/index.php b/examples/06/index.php new file mode 100644 index 0000000..689041d --- /dev/null +++ b/examples/06/index.php @@ -0,0 +1,13 @@ +get('/', function () { + return 'This is a closure controller!'; +}); + +$router->dispatch(); \ No newline at end of file diff --git a/examples/07/index.php b/examples/07/index.php new file mode 100644 index 0000000..a550de1 --- /dev/null +++ b/examples/07/index.php @@ -0,0 +1,28 @@ +get('/method', [UsersController::class, 'index']); + +// Controller: Class=UsersController Method=handle() +$router->get('/class', UsersController::class); + +$router->dispatch(); diff --git a/examples/example-09/index.php b/examples/08/index.php similarity index 79% rename from examples/example-09/index.php rename to examples/08/index.php index dc7ba80..ed6b632 100644 --- a/examples/example-09/index.php +++ b/examples/08/index.php @@ -4,28 +4,24 @@ use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); // Required parameter $router->get('/post/{id}', function ($id) { return "The content of post $id"; }); - // Optional parameter $router->get('/welcome/{name?}', function ($name = null) { return 'Welcome ' . ($name ?: 'Dear User'); }); - -// Optional parameter, Optional Slash! +// Optional parameter, Optional / (Slash)! $router->get('/profile/?{user?}', function ($user = null) { return ($user ?: 'Your') . ' profile'; }); - // Optional parameter with default value -$router->get('/role/{role?}', function ($role = 'admin') { - return "Role is $role"; +$router->get('/roles/{role?}', function ($role = 'guest') { + return "Your role is $role"; }); - // Multiple parameters $router->get('/post/{pid}/comment/{cid}', function ($pid, $cid) { return "The comment $cid of the post $pid"; diff --git a/examples/example-10/index.php b/examples/09/index.php similarity index 81% rename from examples/example-10/index.php rename to examples/09/index.php index 6633264..9d56012 100644 --- a/examples/example-10/index.php +++ b/examples/09/index.php @@ -4,8 +4,9 @@ use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); +// "id" must be numeric $router->pattern('id', '[0-9]+'); $router->get('/post/{id}', function (int $id) { diff --git a/examples/10/index.php b/examples/10/index.php new file mode 100644 index 0000000..9fe62b9 --- /dev/null +++ b/examples/10/index.php @@ -0,0 +1,23 @@ +get('/test', function (ServerRequest $request) { + return new JsonResponse([ + 'method' => $request->getMethod(), + 'uri' => $request->getUri()->getPath(), + 'body' => $request->getBody()->getContents(), + 'parsedBody' => $request->getParsedBody(), + 'headers' => $request->getHeaders(), + 'queryParameters' => $request->getQueryParams(), + 'attributes' => $request->getAttributes(), + ]); +}); + +$router->dispatch(); diff --git a/examples/11/index.php b/examples/11/index.php new file mode 100644 index 0000000..608d7f2 --- /dev/null +++ b/examples/11/index.php @@ -0,0 +1,33 @@ +get('/html/1', function () { + return 'This is an HTML response'; +}); +$router->get('/html/2', function () { + return new HtmlResponse('This is also an HTML response', 200); +}); +$router->get('/json', function () { + return new JsonResponse(['error' => 'Unauthorized!'], 401); +}); +$router->get('/text', function () { + return new TextResponse('This is a plain text...'); +}); +$router->get('/empty', function () { + return new EmptyResponse(204); +}); +$router->get('/redirect', function () { + return new RedirectResponse('https://miladrahimi.com'); +}); + +$router->dispatch(); diff --git a/examples/example-16/index.php b/examples/12/index.php similarity index 58% rename from examples/example-16/index.php rename to examples/12/index.php index a52aacb..0272d2f 100644 --- a/examples/example-16/index.php +++ b/examples/12/index.php @@ -2,36 +2,32 @@ require('../../vendor/autoload.php'); +use MiladRahimi\PhpRouter\Examples\Shared\SimpleController; use MiladRahimi\PhpRouter\Examples\Shared\SimpleMiddleware; use MiladRahimi\PhpRouter\Router; -$router = new Router(); +$router = Router::create(); // A group with uri prefix $router->group(['prefix' => '/admin'], function (Router $router) { // URI: /admin/setting $router->get('/setting', function () { - return 'Setting.'; + return 'Setting Panel'; }); }); -// All of group properties together! +// All of group attributes together! $attributes = [ 'prefix' => '/products', - 'namespace' => 'App\Controllers', 'domain' => 'shop.example.com', - 'middleware' => SimpleMiddleware::class, + 'middleware' => [SimpleMiddleware::class], ]; -// A group with many common properties! $router->group($attributes, function (Router $router) { - // URI: http://shop.example.com/products/{id} - // Controller: App\Controllers\ShopController@getProduct + // URL: http://shop.example.com/products/{id} // Domain: shop.example.com // Middleware: SampleMiddleware - $router->get('/{id}', function ($id) { - return 'Wow.'; - }); + $router->get('/{id}', [SimpleController::class, 'show']); }); $router->dispatch(); diff --git a/examples/example-14/index.php b/examples/13/index.php similarity index 60% rename from examples/example-14/index.php rename to examples/13/index.php index 58db27a..2d60849 100644 --- a/examples/example-14/index.php +++ b/examples/13/index.php @@ -3,15 +3,15 @@ require('../../vendor/autoload.php'); use MiladRahimi\PhpRouter\Router; -use MiladRahimi\PhpRouter\Middleware; use Psr\Http\Message\ServerRequestInterface; use Laminas\Diactoros\Response\JsonResponse; -class AuthMiddleware implements Middleware +class AuthMiddleware { public function handle(ServerRequestInterface $request, Closure $next) { if ($request->getHeader('Authorization')) { + // Check the auth header... return $next($request); } @@ -19,10 +19,12 @@ public function handle(ServerRequestInterface $request, Closure $next) } } -$router = new Router(); +$router = Router::create(); -$router->get('/admin', function () { - return 'This is admin panel!'; -}, AuthMiddleware::class); +$router->group(['middleware' => [AuthMiddleware::class]], function(Router $router) { + $router->get('/admin', function () { + return 'Admin Panel'; + }); +}); -$router->dispatch(); \ No newline at end of file +$router->dispatch(); diff --git a/examples/14/index.php b/examples/14/index.php new file mode 100644 index 0000000..dd04f22 --- /dev/null +++ b/examples/14/index.php @@ -0,0 +1,30 @@ +group(['domain' => 'shop.com'], function(Router $router) { + $router->get('/', function () { + return 'This is shop.com'; + }); +}); + +// Subdomain +$router->group(['domain' => 'admin.shop.com'], function(Router $router) { + $router->get('/', function () { + return 'This is admin.shop.com'; + }); +}); + +// Subdomain with regex pattern +$router->group(['domain' => '(.*).example.com'], function(Router $router) { + $router->get('/', function () { + return 'This is a subdomain'; + }); +}); + +$router->dispatch(); diff --git a/examples/15/index.php b/examples/15/index.php new file mode 100644 index 0000000..97ac1ee --- /dev/null +++ b/examples/15/index.php @@ -0,0 +1,22 @@ +get('/', [SimpleController::class, 'show'], 'home'); +$router->get('/post/{id}', [SimpleController::class, 'show'], 'post'); +$router->get('/links', function (Url $url) { + return new JsonResponse([ + 'about' => $url->make('home'), /* Result: /about */ + 'post1' => $url->make('post', ['id' => 1]), /* Result: /post/1 */ + 'post2' => $url->make('post', ['id' => 2]) /* Result: /post/2 */ + ]); +}); + +$router->dispatch(); diff --git a/examples/16/index.php b/examples/16/index.php new file mode 100644 index 0000000..a0b6361 --- /dev/null +++ b/examples/16/index.php @@ -0,0 +1,24 @@ +get('/{id}', function (Route $route) { + return new JsonResponse([ + 'uri' => $route->getUri(), /* Result: "/1" */ + 'name' => $route->getName(), /* Result: sample */ + 'path' => $route->getPath(), /* Result: "/{id}" */ + 'method' => $route->getMethod(), /* Result: GET */ + 'domain' => $route->getDomain(), /* Result: null */ + 'parameters' => $route->getParameters(), /* Result: {"id": "1"} */ + 'middleware' => $route->getMiddleware(), /* Result: [] */ + 'controller' => $route->getController(), /* Result: {} */ + ]); +}, 'sample'); + +$router->dispatch(); diff --git a/examples/example-20/index.php b/examples/17/index.php similarity index 94% rename from examples/example-20/index.php rename to examples/17/index.php index a065783..136d60e 100644 --- a/examples/example-20/index.php +++ b/examples/17/index.php @@ -6,7 +6,7 @@ use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException; use Laminas\Diactoros\Response\HtmlResponse; -$router = new Router(); +$router = Router::create(); $router->get('/', function () { return 'Home.'; diff --git a/examples/Shared/PostModel.php b/examples/Shared/PostModel.php deleted file mode 100644 index 65f4893..0000000 --- a/examples/Shared/PostModel.php +++ /dev/null @@ -1,14 +0,0 @@ -get('/', function () { - return 'GET method'; - }) - ->post('/', function () { - return 'POST method'; - }) - ->patch('/', function () { - return 'PATCH method'; - }) - ->put('/', function () { - return 'PUT method'; - }) - ->delete('/', function () { - return 'DELETE method'; - }) - ->dispatch(); diff --git a/examples/example-03/index.php b/examples/example-03/index.php deleted file mode 100644 index 7e615bd..0000000 --- a/examples/example-03/index.php +++ /dev/null @@ -1,19 +0,0 @@ -map('GET', '/', function () { - return 'GET method'; - }) - ->map('POST', '/', function () { - return 'POST method'; - }) - ->map('CUSTOM', '/', function () { - return 'CUSTOM method'; - }) - ->dispatch(); diff --git a/examples/example-05/index.php b/examples/example-05/index.php deleted file mode 100644 index a029051..0000000 --- a/examples/example-05/index.php +++ /dev/null @@ -1,18 +0,0 @@ -get('/closure', function () { - return 'Closure as a controller'; -}); - -function func() { - return 'Function as a controller'; -} -$router->get('/function', 'func'); - -$router->dispatch(); \ No newline at end of file diff --git a/examples/example-06/index.php b/examples/example-06/index.php deleted file mode 100644 index 7e29027..0000000 --- a/examples/example-06/index.php +++ /dev/null @@ -1,20 +0,0 @@ -get('/method', 'Controller@method'); - -$router->dispatch(); \ No newline at end of file diff --git a/examples/example-07/index.php b/examples/example-07/index.php deleted file mode 100644 index 41619b0..0000000 --- a/examples/example-07/index.php +++ /dev/null @@ -1,14 +0,0 @@ -get('/ns', 'MiladRahimi\PhpRouter\Examples\Samples\SimpleController@show'); -// OR -$router->get('/ns', SimpleController::class . '@show'); - -$router->dispatch(); diff --git a/examples/example-08/index.php b/examples/example-08/index.php deleted file mode 100644 index 4a99228..0000000 --- a/examples/example-08/index.php +++ /dev/null @@ -1,12 +0,0 @@ -get('/', 'SimpleController@show'); -// PhpRouter looks for MiladRahimi\PhpRouter\Examples\Samples\SimpleController@show - -$router->dispatch(); diff --git a/examples/example-11/index.php b/examples/example-11/index.php deleted file mode 100644 index bbda684..0000000 --- a/examples/example-11/index.php +++ /dev/null @@ -1,34 +0,0 @@ -get('/', function (ServerRequest $request) { - return new JsonResponse([ - 'method' => $request->getMethod(), - 'uri' => $request->getUri(), - 'body' => $request->getBody(), - 'parsedBody' => $request->getParsedBody(), - 'headers' => $request->getHeaders(), - 'queryParameters' => $request->getQueryParams(), - 'attributes' => $request->getAttributes(), - ]); -}); - -$router->post('/posts', function (ServerRequest $request) { - $post = new PostModel(); - $post->title = $request->getQueryParams()['title']; - $post->content = $request->getQueryParams()['content']; - $post->save(); - - return new EmptyResponse(201); -}); - -$router->dispatch(); diff --git a/examples/example-12/index.php b/examples/example-12/index.php deleted file mode 100644 index 907b4fe..0000000 --- a/examples/example-12/index.php +++ /dev/null @@ -1,30 +0,0 @@ -get('/html/1', function () { - return 'This is an HTML response'; - }) - ->get('/html/2', function () { - return new HtmlResponse('This is also an HTML response', 200); - }) - ->get('/json', function () { - return new JsonResponse(['error' => 'Unauthorized!'], 401); - }) - ->get('/text', function () { - return new TextResponse('This is a plain text...'); - }) - ->get('/empty', function () { - return new EmptyResponse(); - }); - -$router->dispatch(); diff --git a/examples/example-13/index.php b/examples/example-13/index.php deleted file mode 100644 index 9aaf027..0000000 --- a/examples/example-13/index.php +++ /dev/null @@ -1,14 +0,0 @@ -get('/redirect', function () { - return new RedirectResponse('https://miladrahimi.com'); - }) - ->dispatch(); diff --git a/examples/example-15/index.php b/examples/example-15/index.php deleted file mode 100644 index 2c1f3a7..0000000 --- a/examples/example-15/index.php +++ /dev/null @@ -1,18 +0,0 @@ -get('/', 'Controller@method', [], 'domain2.com'); - -// Sub-domain -$router->get('/', 'Controller@method', [], 'server2.domain.com'); - -// Sub-domain with regex pattern -$router->get('/', 'Controller@method', [], '(.*).domain.com'); - -$router->dispatch(); \ No newline at end of file diff --git a/examples/example-17/index.php b/examples/example-17/index.php deleted file mode 100644 index 8c6382e..0000000 --- a/examples/example-17/index.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/about', function () { - return 'About the shop.'; -}); - -// URI: /shop/product/{id} -$router->get('/product/{id}', function ($id) { - return 'A product.'; -}); - -$router->dispatch(); diff --git a/examples/example-18/index.php b/examples/example-18/index.php deleted file mode 100644 index 2ac6792..0000000 --- a/examples/example-18/index.php +++ /dev/null @@ -1,26 +0,0 @@ -name('about')->get('/about', function () { - return 'About.'; -}); -$router->name('post')->get('/post/{id}', function ($id) { - return 'Content of the post: ' . $id; -}); -$router->name('home')->get('/', function (Router $router) { - return new JsonResponse([ - 'links' => [ - 'about' => $router->url('about'), /* Result: /about */ - 'post1' => $router->url('post', ['id' => 1]), /* Result: /post/1 */ - 'post2' => $router->url('post', ['id' => 2]) /* Result: /post/2 */ - ] - ]); -}); - -$router->dispatch(); diff --git a/examples/example-19/index.php b/examples/example-19/index.php deleted file mode 100644 index e3ca619..0000000 --- a/examples/example-19/index.php +++ /dev/null @@ -1,30 +0,0 @@ -get('/', function (Router $router) { - return new JsonResponse([ - 'current_page_name' => $router->currentRoute()->getName(), /* Result: home */ - 'current_page_path' => $router->currentRoute()->getPath(), /* Result: / */ - 'current_page_method' => $router->currentRoute()->getMethod(), /* Result: GET */ - 'current_page_domain' => $router->currentRoute()->getDomain(), /* Result: null */ - ]); -}, 'home'); - -$router->get('/{var1}/{var2}', function (Router $router) { - return new JsonResponse([ - 'current_page_name' => $router->currentRoute()->getName(), /* Result: home */ - 'current_page_path' => $router->currentRoute()->getPath(), /* Result: / */ - 'current_page_method' => $router->currentRoute()->getMethod(), /* Result: GET */ - 'current_page_domain' => $router->currentRoute()->getDomain(), /* Result: null */ - 'current_page_params' => $router->currentRoute()->getParameters(), /* Result: null */ - 'current_page_uri' => $router->currentRoute()->getUri(), /* Result: null */ - ]); -}, 'page'); - -$router->dispatch(); diff --git a/src/Router.php b/src/Router.php index 99e34e5..583e42b 100644 --- a/src/Router.php +++ b/src/Router.php @@ -16,6 +16,7 @@ use MiladRahimi\PhpRouter\Services\Publisher; use Psr\Container\ContainerInterface; use Laminas\Diactoros\ServerRequestFactory; +use RuntimeException; class Router { @@ -89,7 +90,6 @@ public function __construct( * Create a new router instance * * @return static - * @throws ContainerException */ public static function create(): self { @@ -99,7 +99,11 @@ public static function create(): self $container->singleton(Repository::class, new Repository()); $container->singleton(Publisher::class, HttpPublisher::class); - return $container->instantiate(Router::class); + try { + return $container->instantiate(Router::class); + } catch (ContainerException $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } } /** From a803720cb14f92a232e121d72fc1ec61da4a3076 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 16:04:18 +0330 Subject: [PATCH 43/52] update Storekeeper.php --- src/Routing/Storekeeper.php | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/Routing/Storekeeper.php b/src/Routing/Storekeeper.php index eeba652..079713d 100644 --- a/src/Routing/Storekeeper.php +++ b/src/Routing/Storekeeper.php @@ -7,7 +7,7 @@ class Storekeeper /** * @var Repository */ - private $store; + private $repository; /** * @var State @@ -17,12 +17,12 @@ class Storekeeper /** * Storekeeper constructor. * - * @param Repository $store + * @param Repository $repository * @param State $state */ - public function __construct(Repository $store, State $state) + public function __construct(Repository $repository, State $state) { - $this->store = $store; + $this->repository = $repository; $this->state = $state; } @@ -36,7 +36,7 @@ public function __construct(Repository $store, State $state) */ public function add(string $method, string $path, $controller, ?string $name = null): void { - $this->store->save( + $this->repository->save( $method, $this->state->getPrefix() . $path, $controller, @@ -46,22 +46,6 @@ public function add(string $method, string $path, $controller, ?string $name = n ); } - /** - * @return Repository - */ - public function getStore(): Repository - { - return $this->store; - } - - /** - * @param Repository $store - */ - public function setStore(Repository $store): void - { - $this->store = $store; - } - /** * @return State */ From bef28f48cc74d70020241605a1da581d23dbd0df Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 16:41:02 +0330 Subject: [PATCH 44/52] remove extra exception --- src/Router.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Router.php b/src/Router.php index 583e42b..3c6e51a 100644 --- a/src/Router.php +++ b/src/Router.php @@ -16,7 +16,6 @@ use MiladRahimi\PhpRouter\Services\Publisher; use Psr\Container\ContainerInterface; use Laminas\Diactoros\ServerRequestFactory; -use RuntimeException; class Router { @@ -99,11 +98,7 @@ public static function create(): self $container->singleton(Repository::class, new Repository()); $container->singleton(Publisher::class, HttpPublisher::class); - try { - return $container->instantiate(Router::class); - } catch (ContainerException $e) { - throw new RuntimeException($e->getMessage(), $e->getCode(), $e); - } + return $container->instantiate(Router::class); } /** From de05664aec83a1f835f2ae7d2e121c70d462f8c2 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 16:44:51 +0330 Subject: [PATCH 45/52] remove extra setters --- src/Routing/State.php | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/src/Routing/State.php b/src/Routing/State.php index cada28e..3394bd7 100644 --- a/src/Routing/State.php +++ b/src/Routing/State.php @@ -37,21 +37,6 @@ public function __construct(string $prefix = '', array $middleware = [], ?string $this->domain = $domain; } - /** - * Create a state from the given array - * - * @param array $attributes - * @return self - */ - public static function createFromArray(array $attributes): self - { - return new static( - $attributes[static::PREFIX ?? ''], - $attributes[static::MIDDLEWARE ?? []], - $attributes[static::DOMAIN ?? null] - ); - } - /** * Append new attributes to the existing ones * @@ -72,14 +57,6 @@ public function getPrefix(): string return $this->prefix; } - /** - * @param string $prefix - */ - public function setPrefix(string $prefix): void - { - $this->prefix = $prefix; - } - /** * @return array */ @@ -88,14 +65,6 @@ public function getMiddleware(): array return $this->middleware; } - /** - * @param array $middleware - */ - public function setMiddleware(array $middleware): void - { - $this->middleware = $middleware; - } - /** * @return string|null */ @@ -103,12 +72,4 @@ public function getDomain(): ?string { return $this->domain; } - - /** - * @param string|null $domain - */ - public function setDomain(?string $domain): void - { - $this->domain = $domain; - } } From 1e395be4c0674631e03eac35279f38bc6802bca5 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 20:59:10 +0330 Subject: [PATCH 46/52] add tests for current route --- README.md | 2 ++ src/Routing/Attributes.php | 10 ++++++++ src/Routing/State.php | 10 +++----- tests/RouteTest.php | 51 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/Routing/Attributes.php create mode 100644 tests/RouteTest.php diff --git a/README.md b/README.md index 121abea..0ea3ab2 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,8 @@ $router->dispatch(); The group attributes will be explained later in this documentation. +You can use [Attributes](src/Routing/Attributes.php) enum, as well. + ### Middleware PhpRouter supports middleware. diff --git a/src/Routing/Attributes.php b/src/Routing/Attributes.php new file mode 100644 index 0000000..2e40729 --- /dev/null +++ b/src/Routing/Attributes.php @@ -0,0 +1,10 @@ +domain = $attributes[State::DOMAIN] ?? null; - $this->prefix .= $attributes[State::PREFIX] ?? ''; - $this->middleware = array_merge($this->middleware, $attributes[State::MIDDLEWARE] ?? []); + $this->domain = $attributes[Attributes::DOMAIN] ?? null; + $this->prefix .= $attributes[Attributes::PREFIX] ?? ''; + $this->middleware = array_merge($this->middleware, $attributes[Attributes::MIDDLEWARE] ?? []); } /** diff --git a/tests/RouteTest.php b/tests/RouteTest.php new file mode 100644 index 0000000..9def91f --- /dev/null +++ b/tests/RouteTest.php @@ -0,0 +1,51 @@ +mockRequest('POST', 'http://shop.com/admin/profile/666'); + + $router = $this->router(); + + $attributes = [ + Attributes::DOMAIN => 'shop.com', + Attributes::MIDDLEWARE => [SampleMiddleware::class], + Attributes::PREFIX => '/admin', + ]; + + $router->group($attributes, function (Router $router) { + $router->post('/profile/{id}', function (Route $route) { + return $route->__toString(); + }, 'admin.profile'); + }); + + $router->dispatch(); + + $expected = [ + 'method' => 'POST', + 'path' => '/admin/profile/{id}', + 'controller' => function () { + return 'Closure'; + }, + 'name' => 'admin.profile', + 'middleware' => [SampleMiddleware::class], + 'domain' => 'shop.com', + 'uri' => '/admin/profile/666', + 'parameters' => ['id' => '666'], + ]; + + $this->assertEquals(json_encode($expected), $this->output($router)); + } +} From b58c21105cdb95213c25e5a7d8b7a7aae11753af Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sun, 10 Jan 2021 21:17:44 +0330 Subject: [PATCH 47/52] add tests for current route --- tests/RouteTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 9def91f..c56bd6a 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -48,4 +48,24 @@ public function test_current_route_for_a_route_with_all_attributes() $this->assertEquals(json_encode($expected), $this->output($router)); } + + public function test_lately_added_attributes_of_route() + { + $this->mockRequest('POST', 'http://shop.com/admin/profile/666'); + + $router = $this->router(); + + $router->post('/admin/profile/{id}', function (Route $route) { + return [ + $route->getParameters(), + $route->getUri(), + ]; + }, 'admin.profile'); + + $router->dispatch(); + + $expected = [['id' => '666'], '/admin/profile/666']; + + $this->assertEquals(json_encode($expected), $this->output($router)); + } } From f1acb35b1bbabce1aab5793cac4cf4b61551f429 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 30 Jan 2021 19:42:06 +0330 Subject: [PATCH 48/52] use methods instead of properties --- src/Routing/Route.php | 16 ++++++++-------- src/Services/HttpPublisher.php | 9 +-------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/Routing/Route.php b/src/Routing/Route.php index 9ee8f7f..37c2386 100644 --- a/src/Routing/Route.php +++ b/src/Routing/Route.php @@ -79,14 +79,14 @@ public function __construct( public function toArray(): array { return [ - 'method' => $this->method, - 'path' => $this->path, - 'controller' => $this->controller, - 'name' => $this->name, - 'middleware' => $this->middleware, - 'domain' => $this->domain, - 'uri' => $this->uri, - 'parameters' => $this->parameters, + 'method' => $this->getMethod(), + 'path' => $this->getPath(), + 'controller' => $this->getController(), + 'name' => $this->getName(), + 'middleware' => $this->getMiddleware(), + 'domain' => $this->getDomain(), + 'uri' => $this->getUri(), + 'parameters' => $this->getParameters(), ]; } diff --git a/src/Services/HttpPublisher.php b/src/Services/HttpPublisher.php index 2d20f51..ffb605c 100644 --- a/src/Services/HttpPublisher.php +++ b/src/Services/HttpPublisher.php @@ -4,13 +4,6 @@ use Psr\Http\Message\ResponseInterface; -/** - * Class HttpPublisher - * HttpPublisher publishes controller responses as over HTTP. - * - * @package MiladRahimi\PhpRouter\Services - * @codeCoverageIgnore - */ class HttpPublisher implements Publisher { /** @@ -18,7 +11,7 @@ class HttpPublisher implements Publisher */ public function publish($content): void { - $content = empty($content) ? '' : $content; + $content = empty($content) ? null : $content; $output = fopen('php://output', 'a'); From ae8e17ea22349f9bf2cec02124f301b9d09583dd Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 30 Jan 2021 20:11:27 +0330 Subject: [PATCH 49/52] add tests --- src/Dispatching/Caller.php | 10 +++++----- tests/ControllerTest.php | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/Dispatching/Caller.php b/src/Dispatching/Caller.php index c36bd09..3d4c76c 100644 --- a/src/Dispatching/Caller.php +++ b/src/Dispatching/Caller.php @@ -67,16 +67,16 @@ public function call($callable) throw new InvalidCallableException('Invalid callable: ' . implode(',', $callable)); } - list($class, $method) = $callable; + [$class, $method] = $callable; if (class_exists($class) == false) { - throw new InvalidCallableException("Class `$callable` not found."); + throw new InvalidCallableException("Class `$class` not found."); } $object = $this->container->instantiate($class); if (method_exists($object, $method) == false) { - throw new InvalidCallableException("Method `$class::$method` not found."); + throw new InvalidCallableException("Method `$class::$method` is not declared."); } $callable = [$object, $method]; @@ -89,11 +89,11 @@ public function call($callable) } } - if (is_object($callable) && !($callable instanceof Closure)) { + if (is_object($callable) && !$callable instanceof Closure) { if (method_exists($callable, 'handle')) { $callable = [$callable, 'handle']; } else { - throw new InvalidCallableException("Method `$callable::handle` not found."); + throw new InvalidCallableException("Method `handle` is not declared."); } } } diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 28eebe3..7194d3b 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -43,6 +43,46 @@ public function test_with_an_invalid_array_as_controller_it_should_fail() $router->get('/', ['invalid', 'array', 'controller']); $this->expectException(InvalidCallableException::class); + $this->expectExceptionMessage('Invalid callable: invalid,array,controller'); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_with_an_invalid_class_as_controller_it_should_fail() + { + $router = $this->router(); + $router->get('/', ['InvalidController', 'show']); + + $this->expectException(InvalidCallableException::class); + $this->expectExceptionMessage('Class `InvalidController` not found.'); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_with_an_int_as_controller_it_should_fail() + { + $router = $this->router(); + $router->get('/', 666); + + $this->expectException(InvalidCallableException::class); + $this->expectExceptionMessage('Invalid callable.'); + $router->dispatch(); + } + + /** + * @throws Throwable + */ + public function test_with_an_handle_less_class_as_controller_it_should_fail() + { + $router = $this->router(); + $router->get('/', SampleController::class); + + $this->expectException(InvalidCallableException::class); + $this->expectExceptionMessage('Method `handle` is not declared.'); $router->dispatch(); } @@ -55,6 +95,7 @@ public function test_with_an_invalid_method_as_controller_it_should_fail() $router->get('/', [SampleController::class, 'invalid']); $this->expectException(InvalidCallableException::class); + $this->expectExceptionMessage('Method `' . SampleController::class . '::invalid` is not declared.'); $router->dispatch(); } From 6a1349ed1ece5f9c961872c17c0d73b4ddebfb52 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 30 Jan 2021 21:36:55 +0330 Subject: [PATCH 50/52] some cleaning --- src/Dispatching/Matcher.php | 2 +- src/Exceptions/InvalidCallableException.php | 5 ----- src/Exceptions/RouteNotFoundException.php | 5 ----- src/Exceptions/UndefinedRouteException.php | 5 ----- src/Services/Publisher.php | 6 ------ 5 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/Dispatching/Matcher.php b/src/Dispatching/Matcher.php index fc5822b..f71c52d 100644 --- a/src/Dispatching/Matcher.php +++ b/src/Dispatching/Matcher.php @@ -139,4 +139,4 @@ private function regexParameter(string $name, array $patterns): string return '(?<' . $name . '>' . $pattern . ')' . $suffix; } -} \ No newline at end of file +} diff --git a/src/Exceptions/InvalidCallableException.php b/src/Exceptions/InvalidCallableException.php index 844d9dc..b22822c 100644 --- a/src/Exceptions/InvalidCallableException.php +++ b/src/Exceptions/InvalidCallableException.php @@ -4,11 +4,6 @@ use Exception; -/** - * Class InvalidControllerException - * - * @package MiladRahimi\PhpRouter\Exceptions - */ class InvalidCallableException extends Exception { // diff --git a/src/Exceptions/RouteNotFoundException.php b/src/Exceptions/RouteNotFoundException.php index 98b9894..bc7fd06 100644 --- a/src/Exceptions/RouteNotFoundException.php +++ b/src/Exceptions/RouteNotFoundException.php @@ -4,11 +4,6 @@ use Exception; -/** - * Class RouteNotFoundException - * - * @package MiladRahimi\PhpRouter\Exceptions - */ class RouteNotFoundException extends Exception { // diff --git a/src/Exceptions/UndefinedRouteException.php b/src/Exceptions/UndefinedRouteException.php index a473a78..868fd1f 100644 --- a/src/Exceptions/UndefinedRouteException.php +++ b/src/Exceptions/UndefinedRouteException.php @@ -4,11 +4,6 @@ use Exception; -/** - * Class UndefinedRouteException - * - * @package MiladRahimi\PhpRouter\Exceptions - */ class UndefinedRouteException extends Exception { // diff --git a/src/Services/Publisher.php b/src/Services/Publisher.php index 385dcaf..873fc28 100644 --- a/src/Services/Publisher.php +++ b/src/Services/Publisher.php @@ -2,12 +2,6 @@ namespace MiladRahimi\PhpRouter\Services; -/** - * Interface Publisher - * Publishers are responsible to publish the response provided by controllers - * - * @package MiladRahimi\PhpRouter\Services - */ interface Publisher { /** From 0d733f2102fac209215e33f591bda6111c81cecd Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 30 Jan 2021 21:59:51 +0330 Subject: [PATCH 51/52] add test for http publisher --- src/Services/HttpPublisher.php | 2 + tests/Services/HttpPublisherTest.php | 59 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/Services/HttpPublisherTest.php diff --git a/src/Services/HttpPublisher.php b/src/Services/HttpPublisher.php index ffb605c..ee46c7e 100644 --- a/src/Services/HttpPublisher.php +++ b/src/Services/HttpPublisher.php @@ -25,6 +25,8 @@ public function publish($content): void fwrite($output, $content->getBody()); } elseif (is_scalar($content)) { fwrite($output, $content); + } elseif ($content === null) { + fwrite($output, ''); } else { fwrite($output, json_encode($content)); } diff --git a/tests/Services/HttpPublisherTest.php b/tests/Services/HttpPublisherTest.php new file mode 100644 index 0000000..6ae9ab4 --- /dev/null +++ b/tests/Services/HttpPublisherTest.php @@ -0,0 +1,59 @@ +get('/', function () { + return 'Hello!'; + }); + $router->dispatch(); + + $this->assertEquals('Hello!', ob_get_clean()); + } + + /** + * @throws Throwable + */ + public function test_publish_a_empty_response() + { + ob_start(); + + $router = Router::create(); + $router->get('/', function () { + // + }); + $router->dispatch(); + + $this->assertEmpty(ob_get_clean()); + } + + /** + * @throws Throwable + */ + public function test_publish_a_array_response() + { + ob_start(); + + $router = Router::create(); + $router->get('/', function () { + return ['a', 'b', 'c']; + }); + $router->dispatch(); + + $this->assertEquals('["a","b","c"]', ob_get_clean()); + } +} From 2e61d7d7e9a97f0643d0c880b94fda8c102de150 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 30 Jan 2021 22:09:26 +0330 Subject: [PATCH 52/52] add more test --- src/Services/HttpPublisher.php | 2 +- tests/Services/HttpPublisherTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Services/HttpPublisher.php b/src/Services/HttpPublisher.php index ee46c7e..8650f35 100644 --- a/src/Services/HttpPublisher.php +++ b/src/Services/HttpPublisher.php @@ -19,7 +19,7 @@ public function publish($content): void http_response_code($content->getStatusCode()); foreach ($content->getHeaders() as $name => $values) { - header($name . ': ' . $content->getHeaderLine($name)); + @header($name . ': ' . $content->getHeaderLine($name)); } fwrite($output, $content->getBody()); diff --git a/tests/Services/HttpPublisherTest.php b/tests/Services/HttpPublisherTest.php index 6ae9ab4..708f34b 100644 --- a/tests/Services/HttpPublisherTest.php +++ b/tests/Services/HttpPublisherTest.php @@ -56,4 +56,21 @@ public function test_publish_a_array_response() $this->assertEquals('["a","b","c"]', ob_get_clean()); } + + /** + * @throws Throwable + */ + public function test_publish_a_standard_response() + { + ob_start(); + + $router = Router::create(); + $router->get('/', function () { + return new JsonResponse(['error' => 'failed'], 400); + }); + + $router->dispatch(); + + $this->assertEquals('{"error":"failed"}', ob_get_clean()); + } }