├── .gitignore
├── src
├── Exception
│ ├── RoutesFileException.php
│ ├── LogicException.php
│ ├── RuntimeException.php
│ ├── InvalidArgumentException.php
│ ├── UnexpectedValueException.php
│ ├── RouteNotFoundException.php
│ ├── BadRequestException.php
│ ├── AjaxRouterException.php
│ ├── MethodNotAllowedException.php
│ └── HttpExceptionTrait.php
├── RouteResolver
│ ├── ControllerMethod.php
│ └── RouteResolver.php
├── Dispatcher.php
├── Psr7
│ └── Psr7RequestAdapter.php
├── Router.php
└── Route.php
├── CHANGELOG.md
├── phpunit.xml
├── composer.json
├── LICENSE
├── tests
└── Unit
│ ├── DispatcherTest.php
│ ├── RouteTest.php
│ ├── RouterTest.php
│ ├── RouteResolver
│ └── RouteResolverTest.php
│ └── Psr7
│ └── Psr7RequestAdapterTest.php
├── .github
└── workflows
│ └── tests.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | .phpunit.result.cache
--------------------------------------------------------------------------------
/src/Exception/RoutesFileException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | */
12 | class RoutesFileException extends AjaxRouterException
13 | {
14 |
15 | }
--------------------------------------------------------------------------------
/src/Exception/LogicException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | */
12 | class LogicException extends AjaxRouterException
13 | {
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/RuntimeException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | */
12 | class RuntimeException extends AjaxRouterException
13 | {
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/Exception/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | */
12 | class InvalidArgumentException extends AjaxRouterException
13 | {
14 |
15 | }
--------------------------------------------------------------------------------
/src/Exception/UnexpectedValueException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | */
12 | class UnexpectedValueException extends AjaxRouterException
13 | {
14 |
15 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 1.0.6 (2022-03-31)
4 |
5 | * Allows getting the current route via `Route::getCurrentRoute`.
6 |
7 | ## 1.0.5 (2022-03-22)
8 |
9 | * Fixed resolving routes actions that defined as a functions.
10 |
11 | ## 1.0.4 (2022-03-13)
12 |
13 | * Fixed compatibility issues with PHP ^8.0 versions.
14 |
15 | ## 1.0.2 (2022-03-04)
16 |
17 | * Integrated Lazzard\Psr7ResponseSender.
18 |
19 | ## 1.0.0 (2022-02-14)
20 |
21 | * First stable release.
22 |
--------------------------------------------------------------------------------
/src/Exception/RouteNotFoundException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | * @internal
12 | */
13 | class RouteNotFoundException extends AjaxRouterException
14 | {
15 | public function __construct($message, $code = 400)
16 | {
17 | parent::__construct($message, $code);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Exception/BadRequestException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | * @internal
12 | */
13 | class BadRequestException extends AjaxRouterException
14 | {
15 | public function __construct($message, $code = 400)
16 | {
17 | parent::__construct($message, $code);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | src/
9 |
10 |
11 |
12 |
13 | tests
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Exception/AjaxRouterException.php:
--------------------------------------------------------------------------------
1 |
12 | * @link https://amranich.dev
13 | */
14 | class AjaxRouterException extends \Exception
15 | {
16 | use HttpExceptionTrait {
17 | HttpExceptionTrait::__construct as private __HttpExceptionTraitConstructor;
18 | }
19 |
20 | public function __construct($message, $code = 500, $headers = [])
21 | {
22 | parent::__construct($message, $code);
23 | $this->__HttpExceptionTraitConstructor($message, $code, $headers);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Exception/MethodNotAllowedException.php:
--------------------------------------------------------------------------------
1 |
10 | * @link https://amranich.dev
11 | * @internal
12 | */
13 | class MethodNotAllowedException extends AjaxRouterException
14 | {
15 |
16 | public function __construct($message, $code = 405, $allowedMethods = [])
17 | {
18 | parent::__construct($message, $code, [
19 | "Allow" => $this->formatAllowHeaderValue($allowedMethods)
20 | ]);
21 | }
22 |
23 | /**
24 | * @param array $methods
25 | * @return string
26 | */
27 | protected function formatAllowHeaderValue(array $methods)
28 | {
29 | if (empty($methods)) {
30 | return '';
31 | }
32 |
33 | return implode(', ', $methods);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Exception/HttpExceptionTrait.php:
--------------------------------------------------------------------------------
1 |
14 | * @link https://amranich.dev
15 | */
16 | trait HttpExceptionTrait
17 | {
18 | public function __construct($message, $code, $headers = [])
19 | {
20 | // check if the headers was already sent
21 | if (!empty($headers) && !headers_sent()) {
22 | http_response_code($code);
23 |
24 | foreach($headers as $name => $value) {
25 | header("$name: $value");
26 | }
27 | }
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "amranich/ajax-router",
3 | "description": "Handle your AJAX requests efficiently",
4 | "keywords": [
5 | "dispatcher",
6 | "router",
7 | "ajax-dispatcher",
8 | "dipatcher-route",
9 | "ajax-request"
10 | ],
11 | "type": "library",
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "El Amrani Chakir",
16 | "email": "contact@amranich.dev",
17 | "homepage": "https://amranich.dev"
18 | }
19 | ],
20 | "minimum-stability": "stable",
21 | "require": {
22 | "php": "^5.6 || ^7.0 || ^8.0",
23 | "psr/http-message": "^1.0",
24 | "lazzard/psr7-response-sender": "^1.0"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^9",
28 | "guzzlehttp/psr7": "^2.1"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "AmraniCh\\AjaxRouter\\": "src/"
33 | }
34 | },
35 | "config": {
36 | "platform": {
37 | "php": "7.4"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 El Amrani Chakir
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/Unit/DispatcherTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder(Router::class)
14 | ->disableOriginalConstructor()
15 | ->getMock();
16 |
17 | $dispatcherMock = $this->getMockBuilder(Dispatcher::class)
18 | ->disableOriginalConstructor()
19 | ->onlyMethods([])
20 | ->getMock();
21 |
22 | $this->assertSame($dispatcherMock, $dispatcherMock->setRouter($routerMock));
23 | $this->assertSame($routerMock, $dispatcherMock->getRouter());
24 | }
25 |
26 | public function test_onException(): void
27 | {
28 | $dispatcherMock = $this->getMockBuilder(Dispatcher::class)
29 | ->disableOriginalConstructor()
30 | ->onlyMethods([])
31 | ->getMock();
32 |
33 | $onException = function() {};
34 |
35 | $this->assertSame($dispatcherMock, $dispatcherMock->onException($onException));
36 | $this->assertSame($onException, $dispatcherMock->getOnExceptionCallback());
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - "master"
8 |
9 | jobs:
10 | build:
11 | name: PHP ${{ matrix.php-versions }}
12 | runs-on: ubuntu-latest
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | php-versions: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0']
17 |
18 | steps:
19 | - name: Setup PHP Action
20 | uses: shivammathur/setup-php@1.8.2
21 | with:
22 | extensions: intl
23 | php-version: "${{ matrix.php-versions }}"
24 | coverage: pcov
25 |
26 | - name: Checkout
27 | uses: actions/checkout@v2
28 |
29 | - name: Validate composer.json
30 | run: "composer validate"
31 |
32 | - name: Install dependencies
33 | run: "composer install --prefer-dist --no-progress --no-suggest"
34 |
35 | - name: Run test suite
36 | run: "vendor/bin/phpunit --coverage-clover=coverage.xml"
37 | if: ${{ matrix.php >= 7.1 }}
38 |
39 | - name: Run test suite for PHP versions < 7.1
40 | run: "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover=coverage.xml"
41 | if: ${{ matrix.php < 7.1 }}
42 |
43 | - name: Upload coverage to Codecov
44 | uses: codecov/codecov-action@v1
45 | with:
46 | token: ${{ secrets.CODECOV_TOKEN }}
47 | file: ./coverage.xml
48 | flags: tests
49 | name: codecov-umbrella
50 | yml: ./codecov.yml
51 | fail_ci_if_error: true
--------------------------------------------------------------------------------
/src/RouteResolver/ControllerMethod.php:
--------------------------------------------------------------------------------
1 |
12 | * @link https://amranich.dev
13 | * @internal
14 | */
15 | class ControllerMethod
16 | {
17 | /** @var string|object */
18 | protected $class;
19 |
20 | /** @var string */
21 | protected $name;
22 |
23 | /**
24 | * @param string|object $class
25 | * @param string $name
26 | */
27 | public function __construct($class, $name)
28 | {
29 | $this->setClass($class);
30 | $this->setName($name);
31 | }
32 |
33 | /**
34 | * @return object|string
35 | */
36 | public function getClass()
37 | {
38 | return $this->class;
39 | }
40 |
41 | /**
42 | * @param object|string $class
43 | *
44 | * @return ControllerMethod
45 | */
46 | public function setClass($class)
47 | {
48 | $this->class = $class;
49 |
50 | return $this;
51 | }
52 |
53 | /**
54 | * @return string
55 | */
56 | public function getName()
57 | {
58 | return $this->name;
59 | }
60 |
61 | /**
62 | * @param string $name
63 | *
64 | * @return ControllerMethod
65 | */
66 | public function setName($name)
67 | {
68 | $this->name = $name;
69 |
70 | return $this;
71 | }
72 |
73 | /**
74 | * Calls and executes the controller method.
75 | *
76 | * @param array $args
77 | *
78 | * @return mixed
79 | * @throws LogicException
80 | */
81 | public function call(array $args)
82 | {
83 | if (is_object($this->class) === false && class_exists($this->class) === false) {
84 | throw new LogicException("Controller class '$this->class' not found.");
85 | }
86 |
87 | if (!method_exists($this->class, $this->name)) {
88 | throw new LogicException(sprintf(
89 | "The method '%s' not exist in controller '%s'.",
90 | $this->name,
91 | is_object($this->class) ? get_class($this->class) : $this->class
92 | )
93 | );
94 | }
95 |
96 | $callable = [
97 | is_string($this->class) ? new $this->class() : $this->class,
98 | $this->name
99 | ];
100 |
101 | return call_user_func_array($callable, $args);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/Dispatcher.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class Dispatcher
18 | {
19 | /** @var Router */
20 | protected $router;
21 |
22 | /** @var callable */
23 | protected $onExceptionCallback;
24 |
25 | /**
26 | * @param Router $router
27 | */
28 | public function __construct(Router $router)
29 | {
30 | $this->setRouter($router);
31 | }
32 |
33 | /**
34 | * @return Router
35 | */
36 | public function getRouter()
37 | {
38 | return $this->router;
39 | }
40 |
41 | /**
42 | * @return callable
43 | */
44 | public function getOnExceptionCallback()
45 | {
46 | return $this->onExceptionCallback;
47 | }
48 |
49 | /**
50 | * @param Router $router
51 | *
52 | * @return Dispatcher
53 | */
54 | public function setRouter(Router $router)
55 | {
56 | $this->router = $router;
57 |
58 | return $this;
59 | }
60 |
61 | /**
62 | * Executes the route action and generate the response.
63 | *
64 | * @return Dispatcher
65 | * @throws \Exception
66 | */
67 | public function dispatch()
68 | {
69 | $handler = $this->router->run();
70 |
71 | $response = $this->handleException($handler);
72 |
73 | if ($response instanceof ResponseInterface) {
74 | $sender = new Sender;
75 | $sender($response);
76 | }
77 |
78 | if (is_string($response)) {
79 | echo $response;
80 | }
81 |
82 | return $this;
83 | }
84 |
85 | /**
86 | * Allows using a custom exception handler for exceptions that may throw when calling the route actions.
87 | *
88 | * @param callable $callable a callback function that will accept the exception as a first argument.
89 | *
90 | * @return Dispatcher
91 | */
92 | public function onException(callable $callable)
93 | {
94 | $this->onExceptionCallback = $callable;
95 |
96 | return $this;
97 | }
98 |
99 | /**
100 | * handle exceptions that may be thrown during the callback call.
101 | *
102 | * @param \Closure $callback
103 | *
104 | * @return mixed
105 | * @throws \Exception
106 | */
107 | protected function handleException(\Closure $callback)
108 | {
109 | if (!is_callable($this->onExceptionCallback)) {
110 | return $callback();
111 | }
112 |
113 | try {
114 | return $callback();
115 | } catch (\Exception $ex) {
116 | return call_user_func($this->onExceptionCallback, $ex);
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Psr7/Psr7RequestAdapter.php:
--------------------------------------------------------------------------------
1 |
12 | * @link https://amranich.dev
13 | * @internal
14 | */
15 | class Psr7RequestAdapter
16 | {
17 | /** @var ServerRequestInterface */
18 | protected $request;
19 |
20 | /**
21 | * @param ServerRequestInterface $request
22 | */
23 | public function __construct(ServerRequestInterface $request)
24 | {
25 | $this->request = $request;
26 | }
27 |
28 | /**
29 | * Gets request variables.
30 | *
31 | * @return array
32 | */
33 | public function getVariables()
34 | {
35 | if ($this->request->getMethod() === 'GET') {
36 | return $this->getUriQueryVariables();
37 | }
38 |
39 | return array_merge($this->getBodyVariables(), $this->getUriQueryVariables());
40 | }
41 |
42 | /**
43 | * @return array
44 | */
45 | protected function getUriQueryVariables()
46 | {
47 | if ($this->isJsonContentType()) {
48 | // because of the content passed in the URL we need to decode it
49 | $query = urldecode($this->request->getUri()->getQuery());
50 | return json_decode($query, true) ?: [];
51 | }
52 |
53 | return $this->request->getQueryParams();
54 | }
55 |
56 | /**
57 | * @return array
58 | */
59 | protected function getBodyVariables()
60 | {
61 | if ($this->isJsonContentType()) {
62 | return json_decode($this->request->getBody()->getContents(), true);
63 | }
64 |
65 | if ($this->isUrlencodedContentType()) {
66 | if (($body = $this->request->getParsedBody()) === '') {
67 | return [];
68 | }
69 |
70 | return $body;
71 | }
72 |
73 | if ($this->isFormDataContentType()) {
74 | return array_merge(
75 | $this->request->getParsedBody(),
76 | $this->request->getUploadedFiles()
77 | );
78 | }
79 |
80 | // TODO should throw an exception if response content type is not present
81 | // or it value is invalid ?
82 |
83 | return [];
84 | }
85 |
86 | /**
87 | * Checks if the request content type is JSON type.
88 | *
89 | * @return bool
90 | */
91 | protected function isJsonContentType()
92 | {
93 | return $this->isHeaderContains(
94 | 'Content-Type',
95 | 'application/json'
96 | );
97 | }
98 |
99 | /**
100 | * @return bool
101 | */
102 | protected function isUrlencodedContentType()
103 | {
104 | return $this->isHeaderContains(
105 | 'Content-Type',
106 | 'application/x-www-form-urlencoded'
107 | );
108 | }
109 |
110 | /**
111 | * @return bool
112 | */
113 | protected function isFormDataContentType()
114 | {
115 | return $this->isHeaderContains(
116 | 'Content-Type',
117 | 'multipart/form-data'
118 | );
119 | }
120 |
121 | /**
122 | * @return bool
123 | */
124 | protected function isHeaderContains($header, $value)
125 | {
126 | if (!$this->request->hasHeader($header)) {
127 | return false;
128 | }
129 |
130 | return strpos($this->request->getHeaderLine($header), $value) !== false;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/RouteResolver/RouteResolver.php:
--------------------------------------------------------------------------------
1 |
17 | * @link https://amranich.dev
18 | */
19 | class RouteResolver
20 | {
21 | /** @var ServerRequestInterface */
22 | protected $request;
23 |
24 | /** @var array */
25 | protected $variables;
26 |
27 | /** @var array */
28 | protected $controllers;
29 |
30 | /**
31 | * @param ServerRequestInterface $request
32 | * @param array $variables
33 | * @param array $controllers
34 | */
35 | public function __construct(ServerRequestInterface $request, array $variables, array $controllers)
36 | {
37 | $this->request = $request;
38 | $this->variables = $variables;
39 | $this->controllers = $controllers;
40 | }
41 |
42 | /**
43 | * @param Route $route
44 | *
45 | * @return \Closure
46 | * @throws UnexpectedValueException|LogicException
47 | */
48 | public function resolve(Route $route)
49 | {
50 | $value = $route->getValue();
51 |
52 | if (is_string($value)) {
53 | return $this->resolveString($value);
54 | }
55 |
56 | if (is_array($value)) {
57 | return $this->resolveArray($value);
58 | }
59 |
60 | if ($value instanceof \Closure) {
61 | return $this->resolveFunction($value);
62 | }
63 |
64 | throw new UnexpectedValueException(sprintf(
65 | "Unexpected handler value, expecting string/array/function '%s' given.",
66 | gettype($route)
67 | ));
68 | }
69 |
70 | /**
71 | * Resolves routes values that defined as a string.
72 | *
73 | * @param string $string
74 | *
75 | * @return \Closure
76 | * @throws LogicException
77 | */
78 | protected function resolveString($string)
79 | {
80 | $tokens = @explode('@', $string);
81 |
82 | $controller = $tokens[0];
83 | $method = $tokens[1];
84 |
85 | $registeredController = $this->getRegisteredControllerByName($controller);
86 |
87 | if (!$registeredController) {
88 | throw new LogicException("Controller class '$controller' not registered.");
89 | }
90 |
91 | $method = $this->getCallableMethod($registeredController, $method);
92 |
93 | return function () use ($method) {
94 | return call_user_func($method, [$this->variables, $this->request]);
95 | };
96 | }
97 |
98 | /**
99 | * Resolve routes actions that defined as an array of class and method name.
100 | *
101 | * @param array $array
102 | *
103 | * @return \Closure
104 | */
105 | protected function resolveArray(array $array)
106 | {
107 | $method = $this->getCallableMethod($array[0], $array[1]);
108 | return function () use ($method) {
109 | return call_user_func($method, [$this->variables, $this->request]);
110 | };
111 | }
112 |
113 | /**
114 | * Resolve routes actions that defined a functions.
115 | *
116 | * @param array $array
117 | *
118 | * @return \Closure
119 | */
120 | protected function resolveFunction($function)
121 | {
122 | return function () use ($function) {
123 | return call_user_func($function, $this->variables, $this->request);
124 | };
125 | }
126 |
127 | /**
128 | * @param string $controller
129 | * @param string $method
130 | *
131 | * @return \Closure
132 | */
133 | protected function getCallableMethod($controller, $method)
134 | {
135 | $method = new ControllerMethod($controller, $method);
136 |
137 | return function ($args = []) use ($method) {
138 | return $method->call($args);
139 | };
140 | }
141 |
142 | /**
143 | * Gets controller instance from the registered controllers using it name.
144 | *
145 | * @param string $name
146 | *
147 | * @return string|null
148 | */
149 | protected function getRegisteredControllerByName($name)
150 | {
151 | foreach ($this->controllers as $controller) {
152 | if ($this->getControllerName($controller) === $name) {
153 | return $controller;
154 | }
155 | }
156 |
157 | return null;
158 | }
159 |
160 | /**
161 | * Gets controller name.
162 | *
163 | * @param object|string $controller
164 | *
165 | * @return string|null
166 | */
167 | protected function getControllerName($controller)
168 | {
169 | $path = null;
170 |
171 | if (is_object($controller)) {
172 | $class = get_class($controller);
173 | $path = explode('\\', $class);
174 | }
175 |
176 | if (is_string($controller)) {
177 | $path = explode('\\', $controller);
178 | }
179 |
180 | return !$path ?: array_pop($path);
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/tests/Unit/RouteTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder(Route::class)
16 | ->disableOriginalConstructor()
17 | ->onlyMethods([])
18 | ->getMock();
19 |
20 | $methods = ['GET', 'POST'];
21 |
22 | $this->assertSame($routeMock, $routeMock->setMethods($methods));
23 | $this->assertSame($methods, $routeMock->getMethods());
24 | }
25 |
26 | public function test_setMethods_with_unsupported_method(): void
27 | {
28 | $routeMock = $this->getMockBuilder(Route::class)
29 | ->disableOriginalConstructor()
30 | ->onlyMethods([])
31 | ->getMock();
32 |
33 | $methods = ['GET', 'REMOVE', 'PUT'];
34 |
35 | $this->expectException(InvalidArgumentException::class);
36 | $this->expectExceptionCode(500);
37 |
38 | $routeMock->setMethods($methods);
39 | }
40 |
41 | public function test_getMethods(): void
42 | {
43 | $routeMock = $this->getMockBuilder(Route::class)
44 | ->disableOriginalConstructor()
45 | ->onlyMethods([])
46 | ->getMock();
47 |
48 | $methods = ['GET', 'POST'];
49 |
50 | $routeMock->setMethods($methods);
51 |
52 | $this->assertSame($methods, $routeMock->getMethods());
53 | }
54 |
55 | public function test_setName(): void
56 | {
57 | $routeMock = $this->getMockBuilder(Route::class)
58 | ->disableOriginalConstructor()
59 | ->onlyMethods([])
60 | ->getMock();
61 |
62 | $routeMock->setName('getComments');
63 |
64 | $this->assertSame('getComments', $routeMock->getName());
65 | }
66 |
67 | public function test_setName_with_invalid_argument_type(): void
68 | {
69 | $routeMock = $this->getMockBuilder(Route::class)
70 | ->disableOriginalConstructor()
71 | ->onlyMethods([])
72 | ->getMock();
73 |
74 | $this->expectException(InvalidArgumentException::class);
75 | $this->expectExceptionCode(500);
76 |
77 | $routeMock->setName(['getComments']);
78 | }
79 |
80 | public function test_setValue_with_string_value(): void
81 | {
82 | $routeMock = $this->getMockBuilder(Route::class)
83 | ->disableOriginalConstructor()
84 | ->onlyMethods([])
85 | ->getMock();
86 |
87 | $this->assertSame($routeMock, $routeMock->setValue('CommmentController@addComment'));
88 | $this->assertSame('CommmentController@addComment', $routeMock->getValue());
89 | }
90 |
91 | public function test_setValue_with_callable_value(): void
92 | {
93 | $routeMock = $this->getMockBuilder(Route::class)
94 | ->disableOriginalConstructor()
95 | ->onlyMethods([])
96 | ->getMock();
97 |
98 | $this->assertSame($routeMock, $routeMock->setValue(function () {
99 | }));
100 | $this->assertInstanceOf(\Closure::class, $routeMock->getValue());
101 | }
102 |
103 | public function test_setValue_with_invalid_value_argument_type(): void
104 | {
105 | $routeMock = $this->getMockBuilder(Route::class)
106 | ->disableOriginalConstructor()
107 | ->onlyMethods([])
108 | ->getMock();
109 |
110 | $this->expectException(InvalidArgumentException::class);
111 | $this->expectExceptionCode(500);
112 |
113 | $routeMock->setValue(new \stdClass);
114 | }
115 |
116 | public function test_get_static(): void
117 | {
118 | $methods = ['GET'];
119 | $name = 'getPosts';
120 | $value = 'PostController@getPosts';
121 |
122 | $route = Route::get($name, $value);
123 |
124 | $this->assertSame($methods, $route->getMethods());
125 | $this->assertSame($name, $route->getName());
126 | $this->assertSame($name, $route->getName());
127 | }
128 |
129 | public function test_post_static(): void
130 | {
131 | $methods = ['POST'];
132 | $name = 'userLogin';
133 | $value = 'UserController@login';
134 |
135 | $route = Route::post($name, $value);
136 |
137 | $this->assertSame($methods, $route->getMethods());
138 | $this->assertSame($name, $route->getName());
139 | $this->assertSame($name, $route->getName());
140 | }
141 |
142 | public function test_many_static(): void
143 | {
144 | $methods = ['GET', 'POST'];
145 | $name = 'getUser';
146 | $value = 'UserController@getUserByID';
147 |
148 | $route = Route::many($methods, $name, $value);
149 |
150 | $this->assertSame($methods, $route->getMethods());
151 | $this->assertSame($name, $route->getName());
152 | $this->assertSame($name, $route->getName());
153 | }
154 |
155 | public function test_setCurrentRoute(): void
156 | {
157 | $route = $this->createMock(Route::class);
158 |
159 | $this->assertNull(Route::setCurrentRoute($route));
160 | $this->assertSame($route, Route::getCurrentRoute($route));
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://packagist.org/packages/amranich/ajax-router)
2 | [](https://github.com/AmraniCh/ajax-dispatcher/actions/workflows/tests.yml)
3 | 
4 |
5 |
6 | ## Getting Started
7 |
8 | ```bash
9 | composer require amranich/ajax-router
10 | ```
11 |
12 | You can copy/paste this code snippet for a quick start.
13 |
14 | We're using [Guzzle PSR-7 interface implementation](https://github.com/guzzle/psr7) here, but you can use any other library you like as long as it implements the same interface.
15 |
16 | ```php
17 | hasHeader('X-requested-with')
38 | || strtolower($request->getHeader('X-requested-with')[0]) !== 'XMLHttpRequest') {
39 | throw new BadRequestException("Accept only AJAX requests.");
40 | }
41 |
42 | // to organize your project, you can put your routes in a separate file like in an array
43 | // and require it in the second parameter of the router constructor.
44 | $router = new Router($request, 'route', [
45 |
46 | // ?route=getPost&id=1005
47 | Route::get('getPost', function ($params) {
48 |
49 | // PSR7 responses are a little annoying to work with, you always have extra HTTP layers
50 | // in your app that extend the base PSR7 response class, think of a class like JsonResponse,
51 | // and in the constructor add the content-type header and pass it to the parent class.
52 | $response = new Response;
53 |
54 | $response->getBody()->write(json_encode([
55 | 'id' => $params['id'],
56 | 'title' => 'Best Places to Visit in Marrakech',
57 | 'description' => 'Example of post description',
58 | 'created_at' => '2022-02-27 03:00:05'
59 | ]));
60 |
61 | return $response->withHeader('Content-type', 'application/json');
62 | }),
63 | ]);
64 |
65 | $dispatcher = new Dispatcher($router);
66 | $dispatcher->dispatch();
67 |
68 | } catch (Exception $ex) {
69 | $response = new Response(
70 | $ex->getCode() ?: 500,
71 | ['Content-type' => 'application/json'],
72 | json_encode(['message' => $ex->getMessage()])
73 | );
74 |
75 | $sender = new Sender;
76 | $sender($response);
77 | }
78 | ```
79 |
80 | ## Usage Tips
81 |
82 | ### Route to controller/class method
83 |
84 | If you like to put the business logic in a separate class or in a controller, you can route your requests to them like this :
85 |
86 | ```php
87 | Route::get('getPost', [PostController::class, 'getPost']);
88 | ```
89 |
90 | Or :
91 |
92 | ```php
93 | Route::get('getPost', 'PostController@getPost');
94 |
95 | // register the controller class or instance in the router
96 | $router->registerControllers([
97 | PostController::class,
98 | ]);
99 | ```
100 |
101 | If the controller/class has some dependencies that must be passed within the constructor, you can still instantiate the
102 | controller on yourself :
103 |
104 | ```php
105 | $router->registerControllers([
106 | new PostController($dependencyOne, $dependencyTwo)
107 | ]);
108 | ```
109 |
110 | ### Catch route actions exceptions
111 |
112 | *I want to catch exceptions that only occurs from my routes actions, and not those thrown by the library or somewhere else, how I can
113 | do that ?*
114 |
115 | Answer :
116 |
117 | ```php
118 | $dispatcher->onException(function (\Exception $ex) {
119 | // $ex exception thrown by a route action
120 | });
121 | ```
122 |
123 | ### Get current route
124 |
125 | You can access the current route object using the static method `getCurrentRoute` of the `Route` class.
126 |
127 | ```php
128 | $route = Router::getCurrentRoute();
129 | $route->getName();
130 | $route->getMethods();
131 | $route->getValue();
132 |
133 | ```
134 |
135 | ## Background
136 |
137 | The idea of the library came to my mind a long time ago when I was mostly developing web applications using just plain PHP, some of these applications were performing a lot of AJAX requests into a single PHP file, that file can have a hundred lines of code that process these requests depending on a function/method name that sent along with the request, so I started to think of what I can do to improve the way that these requests are handled, and improve the code readability and maintainability.
138 |
139 | ## Prizes
140 |
141 | This package wins the PHP Innovation Award (February 2022) Issued by phpclasses.org
142 |
143 | 🏆 Prize :
144 | One elePHPant mascot.
145 | https://www.php.net/elephpant.php
146 |
147 | 📜 Certificate :
148 | https://amranich.dev/certs/phpclasses-february-2022-innovation-award.pdf
149 |
150 | ## They support this project
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/src/Router.php:
--------------------------------------------------------------------------------
1 |
22 | * @link https://amranich.dev
23 | */
24 | class Router
25 | {
26 | /** @var ServerRequestInterface */
27 | protected $request;
28 |
29 | /** @var string */
30 | protected $routeVariable;
31 |
32 | /** @var array */
33 | protected $routes;
34 |
35 | /** @var array */
36 | protected $controllers = [];
37 |
38 | /**
39 | * @param ServerRequestInterface $request A request class that implement the {@see ServerRequestInterface}
40 | * interface.
41 | * @param string $handlerName The route name to be executed.
42 | * @param array $routes
43 | * @param array $controllers
44 | *
45 | * @throws InvalidArgumentException
46 | */
47 | public function __construct(ServerRequestInterface $request, $handlerName, array $routes, array $controllers = [])
48 | {
49 | $this->setRequest($request);
50 | $this->setRouteVariable($handlerName);
51 | $this->setRoutes($routes);
52 | $this->registerControllers($controllers);
53 | }
54 |
55 | /**
56 | * @return ServerRequestInterface
57 | */
58 | public function getRequest()
59 | {
60 | return $this->request;
61 | }
62 |
63 | /**
64 | * @return string
65 | */
66 | public function getRouteVariable()
67 | {
68 | return $this->routeVariable;
69 | }
70 |
71 | /**
72 | * @return array
73 | */
74 | public function getRoutes()
75 | {
76 | return $this->routes;
77 | }
78 |
79 | /**
80 | * Gets registered controllers namespaces.
81 | *
82 | * @return array
83 | */
84 | public function getControllers()
85 | {
86 | return $this->controllers;
87 | }
88 |
89 | /**
90 | * @param ServerRequestInterface $request
91 | *
92 | * @return Router
93 | */
94 | public function setRequest(ServerRequestInterface $request)
95 | {
96 | $this->request = $request;
97 |
98 | return $this;
99 | }
100 |
101 | /**
102 | * @param string $routeVariable
103 | *
104 | * @return Router
105 | * @throws InvalidArgumentException
106 | */
107 | public function setRouteVariable($routeVariable)
108 | {
109 | if (!is_string($routeVariable)) {
110 | throw new InvalidArgumentException(sprintf(
111 | "A route variable must be of type string, '%s' type given.",
112 | gettype($routeVariable)
113 | ));
114 | }
115 |
116 | $this->routeVariable = $routeVariable;
117 |
118 | return $this;
119 | }
120 |
121 | /**
122 | * @param array $routes
123 | *
124 | * @return Router
125 | */
126 | public function setRoutes($routes)
127 | {
128 | $this->routes = $routes;
129 |
130 | return $this;
131 | }
132 |
133 | /**
134 | * Register controllers namespaces/instances.
135 | *
136 | * @param array $controllers An array of controller instances or namespaces.
137 | *
138 | * @return Router
139 | */
140 | public function registerControllers(array $controllers)
141 | {
142 | $this->controllers = $controllers;
143 |
144 | return $this;
145 | }
146 |
147 | /**
148 | * Runs the logic of find the proper handler for the request.
149 | *
150 | * @return \Closure
151 | * @throws BadRequestException|RouteNotFoundException|MethodNotAllowedException|UnexpectedValueException|LogicException
152 | * @internal
153 | */
154 | public function run()
155 | {
156 | $request = new Psr7RequestAdapter($this->request);
157 | $variables = $request->getVariables();
158 |
159 | if (!array_key_exists($this->routeVariable, $variables)) {
160 | throw new BadRequestException(sprintf(
161 | "The route parameter '%s' not found in request variables.",
162 | $this->routeVariable
163 | ));
164 | }
165 |
166 | $routeName = $variables[$this->routeVariable];
167 |
168 | if ($routeName === '') {
169 | throw new BadRequestException("Route name not given.");
170 | }
171 |
172 | foreach ($this->getRoutes() as $route) {
173 | if ($route->getName() !== $routeName) {
174 | continue;
175 | }
176 |
177 | if (!in_array($this->request->getMethod(), $route->getMethods())) {
178 | throw new MethodNotAllowedException(sprintf(
179 | "The handler '%s' is registered for another HTTP request method(s) [%s].",
180 | $route->getName(),
181 | implode(', ', $route->getMethods())
182 | ), 405, $route->getMethods());
183 | }
184 |
185 | Route::setCurrentRoute($route);
186 |
187 | $resolver = new RouteResolver(
188 | $this->getRequest(),
189 | $variables,
190 | $this->getControllers()
191 | );
192 |
193 | return $resolver->resolve($route);
194 | }
195 |
196 | throw new RouteNotFoundException('No Route found for this request.');
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/Route.php:
--------------------------------------------------------------------------------
1 |
13 | * @link https://amranich.dev
14 | */
15 | class Route
16 | {
17 | /** @var Route */
18 | protected static $currentRoute;
19 |
20 | /** @var array */
21 | protected $methods;
22 |
23 | /** @var string */
24 | protected $name;
25 |
26 | /** @var string|callable */
27 | protected $value;
28 |
29 | /**
30 | * HTTP methods supported by a route.
31 | *
32 | * @var array
33 | */
34 | const SUPPORTED_METHODS = [
35 | 'GET',
36 | 'POST',
37 | ];
38 |
39 | /**
40 | * @param array $methods
41 | * @param string $name
42 | * @param string|callable $value
43 | *
44 | * @throws InvalidArgumentException
45 | */
46 | public function __construct($methods, $name, $value)
47 | {
48 | $this->setMethods($methods);
49 | $this->setName($name);
50 | $this->setValue($value);
51 | }
52 |
53 | /**
54 | * Gets route methods.
55 | *
56 | * @return array
57 | */
58 | public function getMethods()
59 | {
60 | return $this->methods;
61 | }
62 |
63 | /**
64 | * Gets route name.
65 | *
66 | * @return string
67 | */
68 | public function getName()
69 | {
70 | return $this->name;
71 | }
72 |
73 | /**
74 | * Gets route value.
75 | *
76 | * @return string|callable
77 | */
78 | public function getValue()
79 | {
80 | return $this->value;
81 | }
82 |
83 | /**
84 | * Set the value of method
85 | *
86 | * @param array $methods
87 | *
88 | * @return Route
89 | * @throws InvalidArgumentException
90 | */
91 | public function setMethods(array $methods)
92 | {
93 | foreach ($methods as $method) {
94 | if (!in_array($method, self::SUPPORTED_METHODS)) {
95 | throw new InvalidArgumentException(sprintf(
96 | "'%s' HTTP request method not supported by a route, the supported methods are [%s].",
97 | $method,
98 | implode(', ', self::SUPPORTED_METHODS)
99 | ));
100 | }
101 | }
102 |
103 | $this->methods = $methods;
104 |
105 | return $this;
106 | }
107 |
108 | /**
109 | * Set the value of name
110 | *
111 | * @return Route
112 | *
113 | * @throws InvalidArgumentException
114 | */
115 | public function setName($name)
116 | {
117 | if (!is_string($name)) {
118 | throw new InvalidArgumentException("The route name must be of type string.");
119 | }
120 |
121 | $this->name = $name;
122 |
123 | return $this;
124 | }
125 |
126 | /**
127 | * Set the value of value
128 | *
129 | * @return Route
130 | *
131 | * @throws InvalidArgumentException
132 | */
133 | public function setValue($value)
134 | {
135 | if (!is_string($value) && !is_array($value) && !is_callable($value)) {
136 | throw new InvalidArgumentException(sprintf(
137 | "A route value type must be either a string/array, giving '%s' for route with name '%s'",
138 | gettype($value),
139 | $this->getName()
140 | ));
141 | }
142 |
143 | $this->value = $value;
144 |
145 | return $this;
146 | }
147 |
148 | /**
149 | * Define routes from the giving file content.
150 | *
151 | * @param string $path
152 | *
153 | * @return array
154 | * @throws RoutesFileException
155 | */
156 | public static function fromFile($path)
157 | {
158 | if (!file_exists($path)) {
159 | throw new RoutesFileException("The routes file ($path) not exists.");
160 | }
161 |
162 | if (!is_readable($path)) {
163 | throw new RoutesFileException("The routes file ($path) is not readable, please check the file permissions.");
164 | }
165 |
166 | $content = include($path);
167 |
168 | if (!is_array($content)) {
169 | throw new RoutesFileException(sprintf(
170 | "The routes file (%s) must return an array of routes objects, '%s' value returned.",
171 | $path,
172 | gettype($content)
173 | ));
174 | }
175 |
176 | return $content;
177 | }
178 |
179 | /**
180 | * @param Route $route
181 | *
182 | * @return void
183 | */
184 | public static function setCurrentRoute(Route $route)
185 | {
186 | self::$currentRoute = $route;
187 | }
188 |
189 | /**
190 | * @return Route
191 | */
192 | public static function getCurrentRoute()
193 | {
194 | return static::$currentRoute;
195 | }
196 |
197 | /**
198 | * Creates route with GET request method.
199 | *
200 | * @param string $name
201 | * @param string|callable $value
202 | *
203 | * @return Route
204 | * @throws InvalidArgumentException
205 | */
206 | public static function get($name, $value)
207 | {
208 | return static::getInstance(['GET'], $name, $value);
209 | }
210 |
211 | /**
212 | * Creates route with POST request method.
213 | *
214 | * @param string $name
215 | * @param string|callable $value
216 | *
217 | * @return Route
218 | * @throws InvalidArgumentException
219 | */
220 | public static function post($name, $value)
221 | {
222 | return static::getInstance(['POST'], $name, $value);
223 | }
224 |
225 | /**
226 | * Creates route with multiple request methods.
227 | *
228 | * @param array $methods
229 | * @param string $name
230 | * @param string|callable $value
231 | *
232 | * @return Route
233 | * @throws InvalidArgumentException
234 | */
235 | public static function many($methods, $name, $value)
236 | {
237 | return static::getInstance($methods, $name, $value);
238 | }
239 |
240 | /**
241 | * Instance factory.
242 | *
243 | * @param array $methods
244 | * @param string $name
245 | * @param string|callable $value
246 | *
247 | * @return Route
248 | * @throws InvalidArgumentException
249 | */
250 | protected static function getInstance($methods, $name, $value)
251 | {
252 | return new static($methods, $name, $value);
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/tests/Unit/RouterTest.php:
--------------------------------------------------------------------------------
1 | createMock(ServerRequestInterface::class);
21 |
22 | $routerMock = $this->getMockBuilder(Router::class)
23 | ->disableOriginalConstructor()
24 | ->onlyMethods([])
25 | ->getMock();
26 |
27 | $this->assertSame($routerMock, $routerMock->setRequest($requestMock));
28 | $this->assertSame($requestMock, $routerMock->getRequest());
29 | }
30 |
31 | public function test_setHandlerName_With_Invalid_Parameter_Type(): void
32 | {
33 | $routerMock = $this->getMockBuilder(Router::class)
34 | ->disableOriginalConstructor()
35 | ->onlyMethods([])
36 | ->getMock();
37 |
38 | $this->expectException(InvalidArgumentException::class);
39 | $this->expectExceptionMessage("A route variable must be of type string, 'array' type given.");
40 | $this->expectExceptionCode(500);
41 |
42 | $routerMock->setRouteVariable(['handler']);
43 | }
44 |
45 | public function test_setHandlerName(): void
46 | {
47 | $routerMock = $this->getMockBuilder(Router::class)
48 | ->disableOriginalConstructor()
49 | ->onlyMethods([])
50 | ->getMock();
51 |
52 | $this->assertSame($routerMock, $routerMock->setRouteVariable('handler'));
53 | $this->assertSame('handler', $routerMock->getRouteVariable());
54 | }
55 |
56 | /**
57 | * @dataProvider routeProvider
58 | */
59 | public function test_setHandlers($handlers): void
60 | {
61 | $routerMock = $this->getMockBuilder(Router::class)
62 | ->disableOriginalConstructor()
63 | ->onlyMethods([])
64 | ->getMock();
65 |
66 | $this->assertSame($routerMock, $routerMock->setRoutes($handlers));
67 | $this->assertSame($handlers, $routerMock->getRoutes());
68 | }
69 |
70 | public function test_registerControllers(): void
71 | {
72 | $routerMock = $this->getMockBuilder(Router::class)
73 | ->disableOriginalConstructor()
74 | ->onlyMethods([])
75 | ->getMock();
76 |
77 | $controllers = [
78 | 'FooController',
79 | 'BarController',
80 | ];
81 |
82 | $routerMock->registerControllers($controllers);
83 |
84 | $this->assertSame($controllers, $routerMock->getControllers());
85 | }
86 |
87 | public function getPSR7GetRequestAdapterMocked($params = [])
88 | {
89 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
90 | ->onlyMethods([
91 | 'getMethod',
92 | 'getQueryParams'
93 | ])
94 | ->getMockForAbstractClass();
95 |
96 | $requestMock->expects($this->once())
97 | ->method('getMethod')
98 | ->willReturn('GET');
99 |
100 | $requestMock->expects($this->once())
101 | ->method('getQueryParams')
102 | ->willReturn($params ?: [
103 | 'function' => 'updateUser',
104 | 'userID' => '5454',
105 | 'firstname' => 'John',
106 | 'lastname' => 'Doe',
107 | ]);
108 |
109 | return $requestMock;
110 | }
111 |
112 | public function test_run_Where_Route_Parameter_Not_Found_In_Request_Variables(): void
113 | {
114 | $routerMock = $this->getMockBuilder(Router::class)
115 | ->disableOriginalConstructor()
116 | ->onlyMethods([])
117 | ->getMock();
118 |
119 | $routerMock
120 | ->setRequest($this->getPSR7GetRequestAdapterMocked())
121 | ->setRouteVariable('key');
122 |
123 | $this->expectException(BadRequestException::class);
124 | $this->expectExceptionMessage("The route parameter 'key' not found in request variables.");
125 | $this->expectExceptionCode(400);
126 |
127 | $routerMock->run();
128 | }
129 |
130 | public function test_run_Where_Route_Name_Not_Found_In_Request_Variables(): void
131 | {
132 | $routerMock = $this->getMockBuilder(Router::class)
133 | ->disableOriginalConstructor()
134 | ->onlyMethods([])
135 | ->getMock();
136 |
137 | $routerMock
138 | ->setRequest($this->getPSR7GetRequestAdapterMocked(['route' => '']))
139 | ->setRouteVariable('route');
140 |
141 | $this->expectException(BadRequestException::class);
142 | $this->expectExceptionMessage("Route name not given.");
143 | $this->expectExceptionCode(400);
144 |
145 | $routerMock->run();
146 | }
147 |
148 | /**
149 | * @dataProvider routeProvider
150 | */
151 | public function test_run_Where_Route_AJAX_Request_Method_Is_Wrong($handlers): void
152 | {
153 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
154 | ->onlyMethods([
155 | 'getMethod',
156 | 'getQueryParams'
157 | ])
158 | ->getMockForAbstractClass();
159 |
160 | $requestMock->expects($this->atLeastOnce())
161 | ->method('getMethod')
162 | ->willReturn('GET');
163 |
164 | $requestMock->expects($this->once())
165 | ->method('getQueryParams')
166 | ->willReturn([
167 | 'function' => 'addUser',
168 | 'firstname' => 'John',
169 | 'lastname' => 'Doe',
170 | ]);
171 |
172 | $routerMock = $this->getMockBuilder(Router::class)
173 | ->setConstructorArgs([$requestMock, 'function', $handlers])
174 | ->onlyMethods([])
175 | ->getMock();
176 |
177 | $this->expectException(MethodNotAllowedException::class);
178 | $this->expectExceptionCode(405);
179 |
180 | $routerMock->run();
181 | }
182 |
183 | /**
184 | * @dataProvider routeProvider
185 | */
186 | public function test_run_Where_Route_Not_Found($handlers): void
187 | {
188 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
189 | ->onlyMethods(['getMethod', 'getQueryParams'])
190 | ->getMockForAbstractClass();
191 |
192 | $requestMock->expects($this->once())
193 | ->method('getMethod')
194 | ->willReturn('GET');
195 |
196 | $requestMock->expects($this->atLeastOnce())
197 | ->method('getQueryParams')
198 | ->willReturn(['function' => 'wrong']);
199 |
200 | $routerMock = $this->getMockBuilder(Router::class)
201 | ->setConstructorArgs([$requestMock, 'function', $handlers])
202 | ->onlyMethods([])
203 | ->getMock();
204 |
205 | $this->expectException(RouteNotFoundException::class);
206 | $this->expectExceptionMessage('No Route found for this request.');
207 | $this->expectExceptionCode(400);
208 |
209 | $routerMock->run();
210 | }
211 |
212 | public function routeProvider(): array
213 | {
214 | return [
215 | [
216 | [
217 | Route::get('getUsers', 'UserController@getAllUsers'),
218 | Route::post('addUser', 'UserController@addUser'),
219 | ],
220 | ],
221 | ];
222 | }
223 | }
--------------------------------------------------------------------------------
/tests/Unit/RouteResolver/RouteResolverTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder(Route::class)
16 | ->disableOriginalConstructor()
17 | ->onlyMethods(['getValue'])
18 | ->getMock();
19 |
20 | $routeMock->expects($this->once())
21 | ->method('getValue')
22 | ->willReturn('PostController@getPosts');
23 |
24 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
25 | ->disableOriginalConstructor()
26 | ->onlyMethods(['resolveString'])
27 | ->getMock();
28 |
29 | $handlerResolverMock->expects($this->once())
30 | ->method('resolveString')
31 | ->with('PostController@getPosts')
32 | ->willReturn(function () {
33 | });
34 |
35 | $this->assertInstanceOf(\Closure::class, $handlerResolverMock->resolve($routeMock));
36 | }
37 |
38 | public function test_resolve_With_Array_Route_Type(): void
39 | {
40 | $routeMock = $this->getMockBuilder(Route::class)
41 | ->disableOriginalConstructor()
42 | ->onlyMethods(['getValue'])
43 | ->getMock();
44 |
45 | $routeMock->expects($this->once())
46 | ->method('getValue')
47 | ->willReturn([\stdClass::class, 'login']);
48 |
49 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
50 | ->disableOriginalConstructor()
51 | ->onlyMethods(['resolveArray'])
52 | ->getMock();
53 |
54 | $handlerResolverMock->expects($this->once())
55 | ->method('resolveArray')
56 | ->with([\stdClass::class, 'login'])
57 | ->willReturn(function () {});
58 |
59 | $this->assertInstanceOf(\Closure::class, $handlerResolverMock->resolve($routeMock));
60 | }
61 |
62 | public function test_resolve_With_Callable_array_Route_Type(): void
63 | {
64 | $class = $this->getMockBuilder(\stdClass::class)
65 | ->addMethods(['foo'])
66 | ->getMock();
67 |
68 | $callable = [get_class($class), 'foo'];
69 |
70 | $routeMock = $this->getMockBuilder(Route::class)
71 | ->disableOriginalConstructor()
72 | ->onlyMethods(['getValue'])
73 | ->getMock();
74 |
75 | $routeMock->expects($this->once())
76 | ->method('getValue')
77 | ->willReturn($callable);
78 |
79 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
80 | ->disableOriginalConstructor()
81 | ->onlyMethods(['resolveArray'])
82 | ->getMock();
83 |
84 | $handlerResolverMock->expects($this->once())
85 | ->method('resolveArray')
86 | ->with($callable)
87 | ->willReturn(function () {
88 | });
89 |
90 | $this->assertIsCallable($handlerResolverMock->resolve($routeMock));
91 | }
92 |
93 | public function test_resolve_With_Invalid_Route_Type(): void
94 | {
95 | $routeMock = $this->getMockBuilder(Route::class)
96 | ->disableOriginalConstructor()
97 | ->onlyMethods(['getValue'])
98 | ->getMock();
99 |
100 | $routeMock->expects($this->once())
101 | ->method('getValue')
102 | ->willReturn(new \stdClass());
103 |
104 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
105 | ->disableOriginalConstructor()
106 | ->onlyMethods([])
107 | ->getMock();
108 |
109 | $this->expectException(UnexpectedValueException::class);
110 | $this->expectExceptionCode(500);
111 |
112 | $this->assertIsCallable($handlerResolverMock->resolve($routeMock));
113 | }
114 |
115 | public function test_getRegisteredControllerByName_Where_Controller_Is_Registered(): void
116 | {
117 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
118 | ->disableOriginalConstructor()
119 | ->getMock();
120 |
121 | $controllers = [
122 | 'App\Controller\FooController',
123 | 'App\Controller\BarController',
124 | ];
125 |
126 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
127 | ->setConstructorArgs([$requestMock, [], $controllers])
128 | ->onlyMethods(['getControllerName'])
129 | ->getMock();
130 |
131 | $handlerResolverMock->expects($this->exactly(2))
132 | ->method('getControllerName')
133 | ->willReturnOnConsecutiveCalls(
134 | 'FooController',
135 | 'BarController'
136 | );
137 |
138 | $method = $this->getReflectedMethod('getRegisteredControllerByName');
139 |
140 | $this->assertSame('App\Controller\BarController', $method->invoke($handlerResolverMock, 'BarController'));
141 | }
142 |
143 | public function test_getRegisteredControllerByName_Where_Controller_Is_Not_Registered(): void
144 | {
145 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
146 | ->disableOriginalConstructor()
147 | ->getMock();
148 |
149 | $controllers = [
150 | 'App\Controller\FooController',
151 | ];
152 |
153 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
154 | ->setConstructorArgs([$requestMock, [], $controllers])
155 | ->onlyMethods(['getControllerName'])
156 | ->getMock();
157 |
158 | $handlerResolverMock->expects($this->once())
159 | ->method('getControllerName')
160 | ->willReturnOnConsecutiveCalls(
161 | 'FooController',
162 | );
163 |
164 | $method = $this->getReflectedMethod('getRegisteredControllerByName');
165 |
166 | $this->assertNull($method->invoke($handlerResolverMock, 'BarController'));
167 | }
168 |
169 | public function test_getControllerName_With_Controller_Name(): void
170 | {
171 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
172 | ->disableOriginalConstructor()
173 | ->getMock();
174 |
175 | $method = $this->getReflectedMethod('getControllerName');
176 |
177 | $this->assertSame('FooController', $method->invoke($handlerResolverMock, 'App\Controller\FooController'));
178 | }
179 |
180 | public function test_getControllerName_With_Controller_Instance(): void
181 | {
182 | $requestMock = $this->getMockBuilder(\AmraniCh\AjaxRouter\Http\Request::class)
183 | ->disableOriginalConstructor()
184 | ->getMock();
185 |
186 | $handlerResolverMock = $this->getMockBuilder(RouteResolver::class)
187 | ->disableOriginalConstructor()
188 | ->getMock();
189 |
190 | $method = $this->getReflectedMethod('getControllerName');
191 |
192 | $this->assertSame('stdClass', $method->invoke($handlerResolverMock, new \stdClass()));
193 | }
194 |
195 | /**
196 | * Gets accessible reflected method for private/protected methods.
197 | *
198 | * @param string $name
199 | *
200 | * @return \ReflectionMethod
201 | * @throws \ReflectionException
202 | */
203 | protected function getReflectedMethod(string $name): \ReflectionMethod
204 | {
205 | $reflectedClass = new \ReflectionClass(RouteResolver::class);
206 |
207 | $method = $reflectedClass->getMethod($name);
208 |
209 | $method->setAccessible(true);
210 |
211 | return $method;
212 | }
213 | }
--------------------------------------------------------------------------------
/tests/Unit/Psr7/Psr7RequestAdapterTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder(ServerRequestInterface::class)
17 | ->onlyMethods([
18 | 'getMethod',
19 | 'hasHeader',
20 | 'getHeaderLine',
21 | 'getBody',
22 | ])
23 | ->getMockForAbstractClass();
24 |
25 | $requestMock->expects($this->once())
26 | ->method('getMethod')
27 | ->willReturn('POST');
28 |
29 | $requestMock->expects($this->atLeastOnce())
30 | ->method('hasHeader')
31 | ->with('Content-Type')
32 | ->willReturn(true);
33 |
34 | $requestMock->expects($this->atLeastOnce())
35 | ->method('getHeaderLine')
36 | ->with('Content-Type')
37 | ->willReturn('application/json;');
38 |
39 | $streamMock = $this->getMockBuilder(StreamInterface::class)
40 | ->onlyMethods(['getContents'])
41 | ->getMockForAbstractClass();
42 |
43 | $streamMock->expects($this->once())
44 | ->method('getContents')
45 | ->willReturn('{"id":64,"firstname":"John","lastname":"Doe"}');
46 |
47 | $requestMock->expects($this->once())
48 | ->method('getBody')
49 | ->willReturn($streamMock);
50 |
51 | $adapter = new Psr7RequestAdapter($requestMock);
52 |
53 | $this->assertSame($adapter->getVariables(), [
54 | 'id' => 64,
55 | 'firstname' => 'John',
56 | 'lastname' => 'Doe'
57 | ]);
58 | }
59 |
60 | public function test_getVariables_With_POST_Method_With_Response_Urlencoded_Content_Type(): void
61 | {
62 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
63 | ->onlyMethods([
64 | 'getMethod',
65 | 'hasHeader',
66 | 'getParsedBody',
67 | ])
68 | ->getMockForAbstractClass();
69 |
70 | $requestMock->expects($this->atLeastOnce())
71 | ->method('getMethod')
72 | ->willReturn('POST');
73 |
74 | $requestMock->expects($this->atLeastOnce())
75 | ->method('hasHeader')
76 | ->with('Content-Type')
77 | ->willReturn(true);
78 |
79 | $requestMock->expects($this->atLeastOnce())
80 | ->method('getHeaderLine')
81 | ->with('Content-Type')
82 | ->willReturn('application/x-www-form-urlencoded;');
83 |
84 | $requestMock->expects($this->once())
85 | ->method('getParsedBody')
86 | ->willReturn([
87 | 'id' => '64',
88 | 'firstname' => 'John',
89 | 'lastname' => 'Doe'
90 | ]);
91 |
92 | $adapter = new Psr7RequestAdapter($requestMock);
93 |
94 | $this->assertSame($adapter->getVariables(), [
95 | 'id' => '64',
96 | 'firstname' => 'John',
97 | 'lastname' => 'Doe'
98 | ]);
99 | }
100 |
101 | public function test_getVariables_With_POST_Method_With_Response_FormData_Content_Type(): void
102 | {
103 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
104 | ->onlyMethods([
105 | 'getMethod',
106 | 'hasHeader',
107 | 'getParsedBody',
108 | 'getUploadedFiles',
109 | ])
110 | ->getMockForAbstractClass();
111 |
112 | $requestMock->expects($this->atLeastOnce())
113 | ->method('getMethod')
114 | ->willReturn('POST');
115 |
116 | $requestMock->expects($this->atLeastOnce())
117 | ->method('hasHeader')
118 | ->with('Content-Type')
119 | ->willReturn(true);
120 |
121 | $requestMock->expects($this->atLeastOnce())
122 | ->method('getHeaderLine')
123 | ->with('Content-Type')
124 | ->willReturn('multipart/form-data;');
125 |
126 | $parsedBody = [
127 | 'id' => '64',
128 | 'firstname' => 'John',
129 | 'lastname' => 'Doe'
130 | ];
131 |
132 | $requestMock->expects($this->once())
133 | ->method('getParsedBody')
134 | ->willReturn([
135 | 'id' => '64',
136 | 'firstname' => 'John',
137 | 'lastname' => 'Doe'
138 | ]);
139 |
140 | $uploadedFiles = [$this->getMockClass(UploadedFileInterface::class)];
141 |
142 | $requestMock->expects($this->once())
143 | ->method('getUploadedFiles')
144 | ->willReturn($uploadedFiles);
145 |
146 | $adapter = new Psr7RequestAdapter($requestMock);
147 |
148 | $this->assertSame([
149 | 'id' => '64',
150 | 'firstname' => 'John',
151 | 'lastname' => 'Doe',
152 | $this->getMockClass(UploadedFileInterface::class)
153 | ], $adapter->getVariables());
154 | }
155 |
156 | public function test_getVariables_With_POST_Method_With_Response_Content_Urlencoded_Content_Type_With_Empty_Body(): void
157 | {
158 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
159 | ->onlyMethods([
160 | 'getMethod',
161 | 'hasHeader',
162 | 'getParsedBody',
163 | ])
164 | ->getMockForAbstractClass();
165 |
166 | $requestMock->expects($this->atLeastOnce())
167 | ->method('getMethod')
168 | ->willReturn('POST');
169 |
170 | $requestMock->expects($this->atLeastOnce())
171 | ->method('hasHeader')
172 | ->with('Content-Type')
173 | ->willReturn(true);
174 |
175 | $requestMock->expects($this->atLeastOnce())
176 | ->method('getHeaderLine')
177 | ->with('Content-Type')
178 | ->willReturn('application/x-www-form-urlencoded;');
179 |
180 | $requestMock->expects($this->once())
181 | ->method('getParsedBody')
182 | ->willReturn('');
183 |
184 | $adapter = new Psr7RequestAdapter($requestMock);
185 |
186 | $this->assertEmpty($adapter->getVariables());
187 | }
188 |
189 | public function test_getVariables_With_POST_Method_Where_Reponse_Content_Type_Header_Not_Present(): void
190 | {
191 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
192 | ->onlyMethods([
193 | 'getMethod',
194 | 'hasHeader',
195 | ])
196 | ->getMockForAbstractClass();
197 |
198 | $requestMock->expects($this->atLeastOnce())
199 | ->method('getMethod')
200 | ->willReturn('POST');
201 |
202 | $requestMock->expects($this->atLeastOnce())
203 | ->method('hasHeader')
204 | ->with('Content-Type')
205 | ->willReturn(false);
206 |
207 | $adapter = new Psr7RequestAdapter($requestMock);
208 |
209 | $this->assertEmpty($adapter->getVariables());
210 | }
211 |
212 | public function test_getVariables_With_Get_HTTP_Method_And_JSON_Response_Content_Type()
213 | {
214 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
215 | ->onlyMethods([
216 | 'getMethod',
217 | 'hasHeader',
218 | 'getHeaderLine',
219 | 'getUri',
220 | 'getQueryParams'
221 | ])
222 | ->getMockForAbstractClass();
223 |
224 | $requestMock->expects($this->once())
225 | ->method('getMethod')
226 | ->willReturn('GET');
227 |
228 | $requestMock->expects($this->once())
229 | ->method('hasHeader')
230 | ->willReturn(true);
231 |
232 | $requestMock->expects($this->once())
233 | ->method('getHeaderLine')
234 | ->willReturn('application/json;');
235 |
236 | $uriInterfaceMock = $this->getMockBuilder(UriInterface::class)
237 | ->onlyMethods(['getQuery'])
238 | ->getMockForAbstractClass();
239 |
240 | $uriInterfaceMock->expects($this->once())
241 | ->method('getQuery')
242 | ->willReturn('{"id":64,"firstname":"John","lastname":"Doe"}');
243 |
244 | $requestMock->expects($this->once())
245 | ->method('getUri')
246 | ->willReturn($uriInterfaceMock);
247 |
248 | $adapter = new Psr7RequestAdapter($requestMock);
249 |
250 | $this->assertSame($adapter->getVariables(), [
251 | 'id' => 64,
252 | 'firstname' => 'John',
253 | 'lastname' => 'Doe'
254 | ]);
255 | }
256 |
257 | public function test_getVariables_With_Get_HTTP_Method_And_Response_Content_Type_Is_Not_JSON()
258 | {
259 | $requestMock = $this->getMockBuilder(ServerRequestInterface::class)
260 | ->onlyMethods([
261 | 'getMethod',
262 | 'hasHeader',
263 | 'getHeaderLine',
264 | 'getUri',
265 | 'getQueryParams'
266 | ])
267 | ->getMockForAbstractClass();
268 |
269 | $requestMock->expects($this->once())
270 | ->method('getMethod')
271 | ->willReturn('GET');
272 |
273 | $requestMock->expects($this->once())
274 | ->method('hasHeader')
275 | ->willReturn(true);
276 |
277 | $requestMock->expects($this->once())
278 | ->method('getHeaderLine')
279 | ->willReturn('application/json;');
280 |
281 | $uriInterfaceMock = $this->getMockBuilder(UriInterface::class)
282 | ->onlyMethods(['getQuery'])
283 | ->getMockForAbstractClass();
284 |
285 | $uriInterfaceMock->expects($this->once())
286 | ->method('getQuery')
287 | ->willReturn('{"id":64,"firstname":"John","lastname":"Doe"}');
288 |
289 | $requestMock->expects($this->once())
290 | ->method('getUri')
291 | ->willReturn($uriInterfaceMock);
292 |
293 | $adapter = new Psr7RequestAdapter($requestMock);
294 |
295 | $this->assertSame($adapter->getVariables(), [
296 | 'id' => 64,
297 | 'firstname' => 'John',
298 | 'lastname' => 'Doe'
299 | ]);
300 | }
301 | }
302 |
--------------------------------------------------------------------------------