├── .github ├── FUNDING.yml └── workflows │ └── run-tests.yml ├── .gitignore ├── LICENCE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Http │ ├── Controller.php │ ├── Http.php │ └── Middleware.php ├── Router.php ├── RouterCommand.php ├── RouterException.php └── RouterRequest.php └── tests ├── Example ├── .htaccess ├── Controllers │ ├── FooController.php │ └── TestController.php ├── Middlewares │ └── TestMiddleware.php ├── index.php ├── travis-ci-apache └── www.conf.default └── RouterTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ['izniburak'] 4 | patreon: # izniburak 5 | open_collective: # izniburak 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://buymeacoff.ee/izniburak'] 14 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.1, 8.2, 8.3] 13 | stability: [prefer-stable] 14 | 15 | name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 26 | coverage: none 27 | 28 | - name: Setup problem matchers 29 | run: | 30 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 31 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 32 | 33 | - name: Install dependencies 34 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction 35 | 36 | - name: Execute tests 37 | run: composer test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.git/ 2 | /.DS_Store 3 | /composer.lock 4 | /vendor/ 5 | /index.php 6 | /.htaccess 7 | .idea 8 | .phpunit.result.cache 9 | 10 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016, İzni Burak Demirtaş 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Router 2 | ``` 3 | _____ _ _ _____ _____ _ 4 | | __ \| | | | __ \ | __ \ | | 5 | | |__) | |__| | |__) | ______ | |__) |___ _ _| |_ ___ _ __ 6 | | ___/| __ | ___/ |______| | _ // _ \| | | | __/ _ \ '__| 7 | | | | | | | | | | \ \ (_) | |_| | || __/ | 8 | |_| |_| |_|_| |_| \_\___/ \__,_|\__\___|_| 9 | 10 | ``` 11 | **PHP Router**, which also has rich features like Middlewares and Controllers is simple and useful router class for PHP. 12 | 13 | ![Tests](https://github.com/izniburak/php-router/actions/workflows/run-tests.yml/badge.svg) 14 | [![Total Downloads](https://poser.pugx.org/izniburak/router/d/total.svg)](https://packagist.org/packages/izniburak/router) 15 | [![Latest Stable Version](https://poser.pugx.org/izniburak/router/v/stable.svg)](https://packagist.org/packages/izniburak/router) 16 | [![Latest Unstable Version](https://poser.pugx.org/izniburak/router/v/unstable.svg)](https://packagist.org/packages/izniburak/router) 17 | [![License](https://poser.pugx.org/izniburak/router/license.svg)](https://packagist.org/packages/izniburak/router) 18 | 19 | ### Features 20 | - Supports GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD, AJAX and ANY request methods 21 | - Easy access and manage Request and Response via `symfony/http-foundation` package. 22 | - Controllers support (Example: HomeController@about) 23 | - Before and after Route Middlewares support 24 | - Static Route Patterns 25 | - Dynamic Route Patterns 26 | - Easy-to-use patterns 27 | - Adding a new pattern supports. (with RegExp) 28 | - Namespaces supports. 29 | - Group Routing 30 | - Custom 404 and Exception handling 31 | - Debug mode (Error message open/close) 32 | 33 | ## Install 34 | 35 | To install **PHP Router**, You can run the following command directly at your project path in your console: 36 | 37 | ``` 38 | $ composer require izniburak/router 39 | ``` 40 | 41 | OR you can add following lines into the `composer.json` file manually: 42 | ```json 43 | { 44 | "require": { 45 | "izniburak/router": "^2.0" 46 | } 47 | } 48 | ``` 49 | Then, run the following command: 50 | ``` 51 | $ composer install 52 | ``` 53 | 54 | ## Example Usage 55 | ```php 56 | require 'vendor/autoload.php'; 57 | 58 | use Buki\Router\Router; 59 | use Symfony\Component\HttpFoundation\Request; 60 | use Symfony\Component\HttpFoundation\Response; 61 | 62 | $router = new Router; 63 | 64 | // For basic GET URI 65 | $router->get('/', function(Request $request, Response $response) { 66 | $response->setContent('Hello World'); 67 | return $response; 68 | 69 | # OR 70 | # return 'Hello World!'; 71 | }); 72 | 73 | // For basic GET URI by using a Controller class. 74 | $router->get('/test', 'TestController@main'); 75 | 76 | // For auto discovering all methods and URIs 77 | $router->controller('/users', 'UserController'); 78 | 79 | $router->run(); 80 | ``` 81 | 82 | ## Docs 83 | Documentation page: [Buki\Router Docs][doc-url] 84 | 85 | Changelogs: [Buki\Router Changelogs][changelog-url] 86 | 87 | ## Support 88 | [izniburak's homepage][author-url] 89 | 90 | [izniburak's twitter][twitter-url] 91 | 92 | ## Licence 93 | [MIT Licence][mit-url] 94 | 95 | ## Contributing 96 | 97 | 1. Fork it ( https://github.com/izniburak/php-router/fork ) 98 | 2. Create your feature branch (git checkout -b my-new-feature) 99 | 3. Commit your changes (git commit -am 'Add some feature') 100 | 4. Push to the branch (git push origin my-new-feature) 101 | 5. Create a new Pull Request 102 | 103 | ## Contributors 104 | 105 | - [izniburak](https://github.com/izniburak) İzni Burak Demirtaş - creator, maintainer 106 | 107 | [mit-url]: http://opensource.org/licenses/MIT 108 | [doc-url]: https://github.com/izniburak/php-router/wiki 109 | [changelog-url]: https://github.com/izniburak/php-router/wiki/Changelogs 110 | [author-url]: http://burakdemirtas.org 111 | [twitter-url]: https://twitter.com/izniburak 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "izniburak/router", 3 | "type": "library", 4 | "description": "simple router class for php", 5 | "keywords": [ 6 | "router", 7 | "route", 8 | "routing" 9 | ], 10 | "homepage": "https://github.com/izniburak/php-router", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "İzni Burak Demirtaş", 15 | "email": "info@burakdemirtas.org", 16 | "homepage": "https://burakdemirtas.org" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "ext-json": "*", 22 | "symfony/http-foundation": "^6.4" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^8.5 || ^9.4" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Buki\\Router\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Buki\\Tests\\": "tests/" 35 | } 36 | }, 37 | "scripts": { 38 | "test": "vendor/bin/phpunit", 39 | "coverage": "vendor/bin/phpunit --coverage-html coverage", 40 | "dev": "cd tests/Example && php -S 127.0.0.1:8000 -t ./" 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | tests/ 17 | 18 | 19 | 20 | 21 | 22 | src/ 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Http/Controller.php: -------------------------------------------------------------------------------- 1 | 6 | * @Web : https://burakdemirtas.org 7 | * @URL : https://github.com/izniburak/php-router 8 | * @Licence: The MIT License (MIT) - Copyright (c) - http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace Buki\Router; 12 | 13 | use Closure; 14 | use Exception; 15 | use ReflectionMethod; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\HttpFoundation\Response; 18 | 19 | /** 20 | * Class Router 21 | * 22 | * @method $this any(string $route, string|array|Closure $callback, array $options = []) 23 | * @method $this get(string $route, string|array|Closure $callback, array $options = []) 24 | * @method $this post(string $route, string|array|Closure $callback, array $options = []) 25 | * @method $this put(string $route, string|array|Closure $callback, array $options = []) 26 | * @method $this delete(string $route, string|array|Closure $callback, array $options = []) 27 | * @method $this patch(string $route, string|array|Closure $callback, array $options = []) 28 | * @method $this head(string $route, string|array|Closure $callback, array $options = []) 29 | * @method $this options(string $route, string|array|Closure $callback, array $options = []) 30 | * @method $this ajax(string $route, string|array|Closure $callback, array $options = []) 31 | * @method $this xget(string $route, string|array|Closure $callback, array $options = []) 32 | * @method $this xpost(string $route, string|array|Closure $callback, array $options = []) 33 | * @method $this xput(string $route, string|array|Closure $callback, array $options = []) 34 | * @method $this xdelete(string $route, string|array|Closure $callback, array $options = []) 35 | * @method $this xpatch(string $route, string|array|Closure $callback, array $options = []) 36 | * 37 | * @package Buki\Router 38 | * @see https://github.com/izniburak/php-router/wiki 39 | */ 40 | class Router 41 | { 42 | /** Router Version */ 43 | const VERSION = '3.0.0'; 44 | 45 | /** @var string $baseFolder Base folder of the project */ 46 | protected string $baseFolder; 47 | 48 | /** @var array $routes Routes list */ 49 | protected array $routes = []; 50 | 51 | /** @var array $groups List of group routes */ 52 | protected array $groups = []; 53 | 54 | /** @var array $patterns Pattern definitions for parameters of Route */ 55 | protected array $patterns = [ 56 | ':all' => '(.*)', 57 | ':any' => '([^/]+)', 58 | ':id' => '(\d+)', 59 | ':int' => '(\d+)', 60 | ':number' => '([+-]?([0-9]*[.])?[0-9]+)', 61 | ':float' => '([+-]?([0-9]*[.])?[0-9]+)', 62 | ':bool' => '(true|false|1|0)', 63 | ':string' => '([\w\-_]+)', 64 | ':slug' => '([\w\-_]+)', 65 | ':uuid' => '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', 66 | ':date' => '([0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]))', 67 | ]; 68 | 69 | /** @var array $namespaces Namespaces of Controllers and Middlewares files */ 70 | protected array $namespaces = [ 71 | 'middlewares' => '', 72 | 'controllers' => '', 73 | ]; 74 | 75 | /** @var array $path Paths of Controllers and Middlewares files */ 76 | protected array $paths = [ 77 | 'controllers' => 'Controllers', 78 | 'middlewares' => 'Middlewares', 79 | ]; 80 | 81 | /** @var string $mainMethod Main method for controller */ 82 | protected string $mainMethod = 'main'; 83 | 84 | /** @var string $cacheFile Cache file */ 85 | protected string $cacheFile = ''; 86 | 87 | /** @var bool $cacheLoaded Cache is loaded? */ 88 | protected bool $cacheLoaded = false; 89 | 90 | /** @var Closure $errorCallback Route error callback function */ 91 | protected Closure $errorCallback; 92 | 93 | /** @var Closure $notFoundCallback Route exception callback function */ 94 | protected Closure $notFoundCallback; 95 | 96 | /** @var array $middlewares General middlewares for per request */ 97 | protected array $middlewares = []; 98 | 99 | /** @var array $routeMiddlewares Route middlewares */ 100 | protected array $routeMiddlewares = []; 101 | 102 | /** @var array $middlewareGroups Middleware Groups */ 103 | protected array $middlewareGroups = []; 104 | 105 | /** @var RouterRequest */ 106 | private RouterRequest $request; 107 | 108 | /** @var bool */ 109 | private bool $debug = false; 110 | 111 | /** 112 | * Router constructor method. 113 | * 114 | * @param array $params 115 | * @param Request|null $request 116 | * @param Response|null $response 117 | */ 118 | public function __construct(array $params = [], Request $request = null, Response $response = null) 119 | { 120 | $this->baseFolder = realpath(getcwd()); 121 | 122 | if (isset($params['debug']) && is_bool($params['debug'])) { 123 | $this->debug = $params['debug']; 124 | } 125 | 126 | // RouterRequest 127 | $request = $request ?? Request::createFromGlobals(); 128 | $response = $response ?? new Response('', Response::HTTP_OK, ['content-type' => 'text/html']); 129 | $this->request = new RouterRequest($request, $response); 130 | 131 | $this->notFoundCallback = function (Request $request, Response $response) { 132 | $response->setStatusCode(Response::HTTP_NOT_FOUND); 133 | $response->setContent('Looks like page not found or something went wrong. Please try again.'); 134 | return $response; 135 | }; 136 | 137 | $this->errorCallback = function (Request $request, Response $response) { 138 | $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); 139 | $response->setContent('Oops! Something went wrong. Please try again.'); 140 | return $response; 141 | }; 142 | 143 | $this->setPaths($params); 144 | $this->loadCache(); 145 | } 146 | 147 | /** 148 | * Add route method; 149 | * Get, Post, Put, Delete, Patch, Any, Ajax... 150 | * 151 | * @param $method 152 | * @param $params 153 | * 154 | * @return mixed 155 | * @throws 156 | */ 157 | public function __call($method, $params) 158 | { 159 | if ($this->cacheLoaded) { 160 | return true; 161 | } 162 | 163 | if (is_null($params)) { 164 | return false; 165 | } 166 | 167 | if (!in_array(strtoupper($method), explode('|', $this->request->validMethods()))) { 168 | $this->exception("Method is not valid. [{$method}]"); 169 | } 170 | 171 | [$route, $callback] = $params; 172 | $options = $params[2] ?? null; 173 | if (str_contains($route, ':')) { 174 | $route1 = $route2 = ''; 175 | foreach (explode('/', $route) as $key => $value) { 176 | if ($value != '') { 177 | if (!strpos($value, '?')) { 178 | $route1 .= '/' . $value; 179 | } else { 180 | if ($route2 == '') { 181 | $this->addRoute($route1, $method, $callback, $options); 182 | } 183 | 184 | $route2 = $route1 . '/' . str_replace('?', '', $value); 185 | $this->addRoute($route2, $method, $callback, $options); 186 | $route1 = $route2; 187 | } 188 | } 189 | } 190 | 191 | if ($route2 == '') { 192 | $this->addRoute($route1, $method, $callback, $options); 193 | } 194 | } else { 195 | $this->addRoute($route, $method, $callback, $options); 196 | } 197 | 198 | return $this; 199 | } 200 | 201 | /** 202 | * Add new route method one or more http methods. 203 | * 204 | * @param string $methods 205 | * @param string $route 206 | * @param string|array|closure $callback 207 | * @param array $options 208 | * 209 | * @return void 210 | */ 211 | public function add(string $methods, string $route, $callback, array $options = []): void 212 | { 213 | if ($this->cacheLoaded) { 214 | return; 215 | } 216 | 217 | if (strstr($methods, '|')) { 218 | foreach (array_unique(explode('|', $methods)) as $method) { 219 | if (!empty($method)) { 220 | $this->addRoute($route, $method, $callback, $options); 221 | } 222 | } 223 | } else { 224 | $this->addRoute($route, $methods, $callback, $options); 225 | } 226 | } 227 | 228 | /** 229 | * Add new route rules pattern; String or Array 230 | * 231 | * @param array|string $pattern 232 | * @param string|null $attr 233 | * 234 | * @return mixed 235 | * @throws 236 | */ 237 | public function pattern(array|string $pattern, string $attr = null) 238 | { 239 | if (is_array($pattern)) { 240 | foreach ($pattern as $key => $value) { 241 | if (in_array($key, array_keys($this->patterns))) { 242 | $this->exception($key . ' pattern cannot be changed.'); 243 | } 244 | $this->patterns[$key] = '(' . $value . ')'; 245 | } 246 | } else { 247 | if (in_array($pattern, array_keys($this->patterns))) { 248 | $this->exception($pattern . ' pattern cannot be changed.'); 249 | } 250 | $this->patterns[$pattern] = '(' . $attr . ')'; 251 | } 252 | 253 | return true; 254 | } 255 | 256 | /** 257 | * Run Routes 258 | * 259 | * @return void 260 | * @throws 261 | */ 262 | public function run(): void 263 | { 264 | try { 265 | $uri = $this->getRequestUri(); 266 | $method = $this->request->getMethod(); 267 | $searches = array_keys($this->patterns); 268 | $replaces = array_values($this->patterns); 269 | $foundRoute = false; 270 | 271 | foreach ($this->routes as $data) { 272 | $route = $data['route']; 273 | if (!$this->request->validMethod($data['method'], $method)) { 274 | continue; 275 | } 276 | 277 | // Direct Route Match 278 | if ($route === $uri) { 279 | $foundRoute = true; 280 | $this->runRouteMiddleware($data, 'before'); 281 | $this->runRouteCommand($data['callback']); 282 | $this->runRouteMiddleware($data, 'after'); 283 | break; 284 | 285 | // Parameter Route Match 286 | } elseif (strstr($route, ':') !== false) { 287 | $route = str_replace($searches, $replaces, $route); 288 | if (preg_match('#^' . $route . '$#', $uri, $matched)) { 289 | $foundRoute = true; 290 | 291 | $this->runRouteMiddleware($data, 'before'); 292 | 293 | array_shift($matched); 294 | $matched = array_map(function ($value) { 295 | return trim(urldecode($value)); 296 | }, $matched); 297 | 298 | foreach ($data['groups'] as $group) { 299 | if (strstr($group, ':') !== false) { 300 | array_shift($matched); 301 | } 302 | } 303 | 304 | $this->runRouteCommand($data['callback'], $matched); 305 | $this->runRouteMiddleware($data, 'after'); 306 | break; 307 | } 308 | } 309 | } 310 | 311 | // If it originally was a HEAD request, clean up after ourselves by emptying the output buffer 312 | if ($this->request()->isMethod('HEAD')) { 313 | ob_end_clean(); 314 | } 315 | 316 | if ($foundRoute === false) { 317 | $this->response()->setStatusCode(Response::HTTP_NOT_FOUND); 318 | $this->routerCommand()->sendResponse( 319 | call_user_func($this->notFoundCallback, $this->request(), $this->response()) 320 | ); 321 | } 322 | } catch (Exception $e) { 323 | if ($this->debug) { 324 | throw $e; 325 | } 326 | $this->response()->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); 327 | $this->routerCommand()->sendResponse( 328 | call_user_func($this->errorCallback, $this->request(), $this->response(), $e) 329 | ); 330 | } 331 | } 332 | 333 | /** 334 | * Routes Group 335 | * 336 | * @param string $prefix 337 | * @param Closure $callback 338 | * @param array $options 339 | * 340 | * @return bool 341 | */ 342 | public function group(string $prefix, Closure $callback, array $options = []): bool 343 | { 344 | if ($this->cacheLoaded) { 345 | return true; 346 | } 347 | 348 | $group = []; 349 | $group['route'] = $this->clearRouteName($prefix); 350 | $group['before'] = $this->calculateMiddleware($options['before'] ?? []); 351 | $group['after'] = $this->calculateMiddleware($options['after'] ?? []); 352 | 353 | array_push($this->groups, $group); 354 | 355 | call_user_func_array($callback, [$this]); 356 | 357 | $this->endGroup(); 358 | 359 | return true; 360 | } 361 | 362 | /** 363 | * Added route from methods of Controller file. 364 | * 365 | * @param string $route 366 | * @param string $controller 367 | * @param array $options 368 | * 369 | * @return void 370 | * @throws 371 | */ 372 | public function controller(string $route, string $controller, array $options = []): void 373 | { 374 | if ($this->cacheLoaded) { 375 | return; 376 | } 377 | 378 | $only = $options['only'] ?? []; 379 | $except = $options['except'] ?? []; 380 | $controller = $this->resolveClassName($controller); 381 | $classMethods = get_class_methods($controller); 382 | if ($classMethods) { 383 | foreach ($classMethods as $methodName) { 384 | if (!str_contains($methodName, '__')) { 385 | $method = 'any'; 386 | foreach (explode('|', $this->request->validMethods()) as $m) { 387 | if (stripos($methodName, $m = strtolower($m), 0) === 0) { 388 | $method = $m; 389 | break; 390 | } 391 | } 392 | 393 | $methodVar = lcfirst( 394 | preg_replace('/' . $method . '_?/i', '', $methodName, 1) 395 | ); 396 | $methodVar = strtolower(preg_replace('%([a-z]|[0-9])([A-Z])%', '\1-\2', $methodVar)); 397 | 398 | if ((!empty($only) && !in_array($methodVar, $only)) 399 | || (!empty($except) && in_array($methodVar, $except))) { 400 | continue; 401 | } 402 | 403 | $ref = new ReflectionMethod($controller, $methodName); 404 | $endpoints = []; 405 | foreach ($ref->getParameters() as $param) { 406 | $typeHint = $param->hasType() ? $param->getType()->getName() : null; 407 | if (!in_array($typeHint, ['int', 'float', 'string', 'bool']) && $typeHint !== null) { 408 | continue; 409 | } 410 | $pattern = isset($this->patterns[":{$typeHint}"]) ? ":{$typeHint}" : ":any"; 411 | $endpoints[] = $param->isOptional() ? "{$pattern}?" : $pattern; 412 | } 413 | 414 | $value = ($methodVar === $this->mainMethod ? $route : $route . '/' . $methodVar); 415 | $this->{$method}( 416 | ($value . '/' . implode('/', $endpoints)), 417 | ($controller . '@' . $methodName), 418 | $options 419 | ); 420 | } 421 | } 422 | unset($ref); 423 | } 424 | } 425 | 426 | /** 427 | * Routes Not Found Error function. 428 | * 429 | * @param Closure $callback 430 | * 431 | * @return void 432 | */ 433 | public function notFound(Closure $callback): void 434 | { 435 | $this->notFoundCallback = $callback; 436 | } 437 | 438 | /** 439 | * Routes exception errors function. 440 | * 441 | * @param Closure $callback 442 | * 443 | * @return void 444 | */ 445 | public function error(Closure $callback): void 446 | { 447 | $this->errorCallback = $callback; 448 | } 449 | 450 | /** 451 | * Display all Routes. 452 | * 453 | * @return void 454 | */ 455 | public function getList(): void 456 | { 457 | $routes = var_export($this->getRoutes(), true); 458 | die("
{$routes}
"); 459 | } 460 | 461 | /** 462 | * Get all Routes 463 | * 464 | * @return array 465 | */ 466 | public function getRoutes(): array 467 | { 468 | return $this->routes; 469 | } 470 | 471 | /** 472 | * Cache all routes 473 | * 474 | * @return bool 475 | * 476 | * @throws Exception 477 | */ 478 | public function cache(): bool 479 | { 480 | foreach ($this->getRoutes() as $key => $route) { 481 | if (!is_string($route['callback'])) { 482 | $this->exception('Routes cannot contain a Closure/Function callback while caching.'); 483 | } 484 | } 485 | 486 | $cacheContent = 'getRoutes(), true) . ';' . PHP_EOL; 487 | if (false === file_put_contents($this->cacheFile, $cacheContent)) { 488 | $this->exception('Routes cache file could not be written.'); 489 | } 490 | 491 | return true; 492 | } 493 | 494 | /** 495 | * Set general middlewares 496 | * 497 | * @param array $middlewares 498 | * 499 | * @return void 500 | */ 501 | public function setMiddleware(array $middlewares): void 502 | { 503 | $this->middlewares = $middlewares; 504 | } 505 | 506 | /** 507 | * Set Route middlewares 508 | * 509 | * @param array $middlewares 510 | * 511 | * @return void 512 | */ 513 | public function setRouteMiddleware(array $middlewares): void 514 | { 515 | $this->routeMiddlewares = $middlewares; 516 | } 517 | 518 | /** 519 | * Set middleware groups 520 | * 521 | * @param array $middlewareGroup 522 | * 523 | * @return void 524 | */ 525 | public function setMiddlewareGroup(array $middlewareGroup): void 526 | { 527 | $this->middlewareGroups = $middlewareGroup; 528 | } 529 | 530 | /** 531 | * Get All Middlewares 532 | * 533 | * @return array 534 | */ 535 | public function getMiddlewares(): array 536 | { 537 | return [ 538 | 'middlewares' => $this->middlewares, 539 | 'routeMiddlewares' => $this->routeMiddlewares, 540 | 'middlewareGroups' => $this->middlewareGroups, 541 | ]; 542 | } 543 | 544 | /** 545 | * Detect Routes Middleware; before or after 546 | * 547 | * @param array $middleware 548 | * @param string $type 549 | * 550 | * @return void 551 | */ 552 | protected function runRouteMiddleware(array $middleware, string $type): void 553 | { 554 | $this->routerCommand()->beforeAfter($middleware[$type]); 555 | } 556 | 557 | /** 558 | * @return Request 559 | */ 560 | protected function request(): Request 561 | { 562 | return $this->request->symfonyRequest(); 563 | } 564 | 565 | /** 566 | * @return Response 567 | */ 568 | protected function response(): Response 569 | { 570 | return $this->request->symfonyResponse(); 571 | } 572 | 573 | /** 574 | * Throw new Exception for Router Error 575 | * 576 | * @param string $message 577 | * @param int $statusCode 578 | * 579 | * @throws Exception 580 | */ 581 | protected function exception(string $message = '', int $statusCode = Response::HTTP_INTERNAL_SERVER_ERROR) 582 | { 583 | throw new RouterException($message, $statusCode); 584 | } 585 | 586 | /** 587 | * RouterCommand class 588 | * 589 | * @return RouterCommand 590 | */ 591 | protected function routerCommand(): RouterCommand 592 | { 593 | return RouterCommand::getInstance( 594 | $this->baseFolder, $this->paths, $this->namespaces, 595 | $this->request(), $this->response(), 596 | $this->getMiddlewares() 597 | ); 598 | } 599 | 600 | /** 601 | * Set paths and namespaces for Controllers and Middlewares. 602 | * 603 | * @param array $params 604 | * 605 | * @return void 606 | */ 607 | protected function setPaths(array $params): void 608 | { 609 | if (empty($params)) { 610 | return; 611 | } 612 | 613 | if (isset($params['paths']) && $paths = $params['paths']) { 614 | $this->paths['controllers'] = isset($paths['controllers']) 615 | ? rtrim($paths['controllers'], '/') 616 | : $this->paths['controllers']; 617 | 618 | $this->paths['middlewares'] = isset($paths['middlewares']) 619 | ? rtrim($paths['middlewares'], '/') 620 | : $this->paths['middlewares']; 621 | } 622 | 623 | if (isset($params['namespaces']) && $namespaces = $params['namespaces']) { 624 | $this->namespaces['controllers'] = isset($namespaces['controllers']) 625 | ? rtrim($namespaces['controllers'], '\\') . '\\' 626 | : ''; 627 | 628 | $this->namespaces['middlewares'] = isset($namespaces['middlewares']) 629 | ? rtrim($namespaces['middlewares'], '\\') . '\\' 630 | : ''; 631 | } 632 | 633 | if (isset($params['base_folder'])) { 634 | $this->baseFolder = rtrim($params['base_folder'], '/'); 635 | } 636 | 637 | $basePath = str_replace($this->request()->server->get('DOCUMENT_ROOT'), '', $this->baseFolder); 638 | if (($baseFolder = $this->clearRouteName($basePath)) !== '/') { 639 | $this->baseFolder = $baseFolder; 640 | } 641 | 642 | if (isset($params['main_method'])) { 643 | $this->mainMethod = $params['main_method']; 644 | } 645 | 646 | $this->cacheFile = $params['cache'] ?? realpath(__DIR__ . '/../cache.php'); 647 | } 648 | 649 | /** 650 | * @param string $controller 651 | * 652 | * @return RouterException|string 653 | * @throws Exception 654 | */ 655 | protected function resolveClassName(string $controller) 656 | { 657 | $controller = str_replace([$this->namespaces['controllers'], '\\', '.'], ['', '/', '/'], $controller); 658 | $controller = trim( 659 | preg_replace( 660 | '/' . str_replace('/', '\\/', $this->paths['controllers']) . '/i', 661 | '', 662 | $controller, 663 | 1 664 | ), 665 | '/' 666 | ); 667 | 668 | $file = realpath("{$this->paths['controllers']}/{$controller}.php"); 669 | if (!file_exists($file)) { 670 | $this->exception("{$controller} class is not found! Please check the file."); 671 | } 672 | 673 | $controller = $this->namespaces['controllers'] . str_replace('/', '\\', $controller); 674 | if (!class_exists($controller)) { 675 | require_once $file; 676 | } 677 | 678 | return $controller; 679 | } 680 | 681 | /** 682 | * Load Cache file 683 | * 684 | * @return bool 685 | */ 686 | protected function loadCache(): bool 687 | { 688 | if (file_exists($this->cacheFile)) { 689 | $this->routes = require_once $this->cacheFile; 690 | $this->cacheLoaded = true; 691 | return true; 692 | } 693 | 694 | return false; 695 | } 696 | 697 | /** 698 | * Add new Route and it's settings 699 | * 700 | * @param string $uri 701 | * @param string $method 702 | * @param string|array|Closure $callback 703 | * @param array|null $options 704 | * 705 | * @return void 706 | */ 707 | protected function addRoute(string $uri, string $method, $callback, ?array $options = null) 708 | { 709 | $groupUri = ''; 710 | $groupStack = []; 711 | $beforeMiddlewares = []; 712 | $afterMiddlewares = []; 713 | if (!empty($this->groups)) { 714 | foreach ($this->groups as $key => $value) { 715 | $groupUri .= $value['route']; 716 | $groupStack[] = trim($value['route'], '/'); 717 | $beforeMiddlewares = array_merge($beforeMiddlewares, $value['before']); 718 | $afterMiddlewares = array_merge($afterMiddlewares, $value['after']); 719 | } 720 | } 721 | 722 | $beforeMiddlewares = array_merge($beforeMiddlewares, $this->calculateMiddleware($options['before'] ?? [])); 723 | $afterMiddlewares = array_merge($afterMiddlewares, $this->calculateMiddleware($options['after'] ?? [])); 724 | 725 | $callback = is_array($callback) ? implode('@', $callback) : $callback; 726 | $routeName = is_string($callback) 727 | ? strtolower(preg_replace( 728 | '/[^\w]/i', '.', str_replace($this->namespaces['controllers'], '', $callback) 729 | )) 730 | : null; 731 | $data = [ 732 | 'route' => $this->clearRouteName("{$groupUri}/{$uri}"), 733 | 'method' => strtoupper($method), 734 | 'callback' => $callback, 735 | 'name' => $options['name'] ?? $routeName, 736 | 'before' => $beforeMiddlewares, 737 | 'after' => $afterMiddlewares, 738 | 'groups' => $groupStack, 739 | ]; 740 | array_unshift($this->routes, $data); 741 | } 742 | 743 | /** 744 | * @param array|string|null $middleware 745 | * 746 | * @return array 747 | */ 748 | protected function calculateMiddleware(array|string|null $middleware): array 749 | { 750 | if (is_null($middleware)) { 751 | return []; 752 | } 753 | 754 | return is_array($middleware) ? $middleware : [$middleware]; 755 | } 756 | 757 | /** 758 | * Run Route Command; Controller or Closure 759 | * 760 | * @param $command 761 | * @param array $params 762 | * 763 | * @return void 764 | * @throws Exception 765 | */ 766 | protected function runRouteCommand($command, array $params = []): void 767 | { 768 | $this->routerCommand()->runRoute($command, $params); 769 | } 770 | 771 | /** 772 | * Routes Group endpoint 773 | * 774 | * @return void 775 | */ 776 | protected function endGroup(): void 777 | { 778 | array_pop($this->groups); 779 | } 780 | 781 | /** 782 | * @param string $route 783 | * 784 | * @return string 785 | */ 786 | protected function clearRouteName(string $route = ''): string 787 | { 788 | $route = trim(preg_replace('~/{2,}~', '/', $route), '/'); 789 | return $route === '' ? '/' : "/{$route}"; 790 | } 791 | 792 | /** 793 | * @return string 794 | */ 795 | protected function getRequestUri(): string 796 | { 797 | $script = $this->request()->server->get('SCRIPT_FILENAME') ?? $this->request()->server->get('SCRIPT_NAME'); 798 | $dirname = dirname($script); 799 | $dirname = $dirname === '/' ? '' : $dirname; 800 | $basename = basename($script); 801 | 802 | $uri = str_replace([$dirname, $basename], '', $this->request()->server->get('REQUEST_URI')); 803 | $uri = preg_replace('/' . str_replace(['\\', '/', '.',], ['/', '\/', '\.'], $this->baseFolder) . '/', '', $uri, 1); 804 | 805 | return $this->clearRouteName(explode('?', $uri)[0]); 806 | } 807 | } 808 | -------------------------------------------------------------------------------- /src/RouterCommand.php: -------------------------------------------------------------------------------- 1 | baseFolder = $baseFolder; 61 | $this->paths = $paths; 62 | $this->namespaces = $namespaces; 63 | $this->request = $request; 64 | $this->response = $response; 65 | $this->middlewares = $middlewares; 66 | 67 | // Execute general Middlewares 68 | foreach ($this->middlewares['middlewares'] as $middleware) { 69 | $this->beforeAfter($middleware); 70 | } 71 | 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | public function getMiddlewareInfo(): array 78 | { 79 | return [ 80 | 'path' => "{$this->paths['middlewares']}", 81 | 'namespace' => $this->namespaces['middlewares'], 82 | ]; 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function getControllerInfo(): array 89 | { 90 | return [ 91 | 'path' => "{$this->paths['controllers']}", 92 | 'namespace' => $this->namespaces['controllers'], 93 | ]; 94 | } 95 | 96 | /** 97 | * @param string $baseFolder 98 | * @param array $paths 99 | * @param array $namespaces 100 | * @param Request $request 101 | * @param Response $response 102 | * @param array $middlewares 103 | * 104 | * @return RouterCommand 105 | */ 106 | public static function getInstance( 107 | string $baseFolder, 108 | array $paths, 109 | array $namespaces, 110 | Request $request, 111 | Response $response, 112 | array $middlewares 113 | ): ?RouterCommand 114 | { 115 | if (null === self::$instance) { 116 | self::$instance = new static( 117 | $baseFolder, $paths, $namespaces, 118 | $request, $response, $middlewares 119 | ); 120 | } 121 | 122 | return self::$instance; 123 | } 124 | 125 | /** 126 | * Run Route Middlewares 127 | * 128 | * @param $command 129 | * 130 | * @return mixed|void 131 | * @throws 132 | */ 133 | public function beforeAfter($command) 134 | { 135 | if (empty($command)) { 136 | return; 137 | } 138 | 139 | $info = $this->getMiddlewareInfo(); 140 | if (is_array($command)) { 141 | foreach ($command as $value) { 142 | $this->beforeAfter($value); 143 | } 144 | } elseif (is_string($command)) { 145 | $middleware = explode(':', $command); 146 | $params = []; 147 | if (count($middleware) > 1) { 148 | $params = explode(',', $middleware[1]); 149 | } 150 | 151 | $resolvedMiddleware = $this->resolveMiddleware($middleware[0]); 152 | $response = false; 153 | if (is_array($resolvedMiddleware)) { 154 | foreach ($resolvedMiddleware as $middleware) { 155 | $response = $this->runMiddleware( 156 | $command, 157 | $this->resolveMiddleware($middleware), 158 | $params, 159 | $info 160 | ); 161 | } 162 | return $response; 163 | } 164 | 165 | return $this->runMiddleware($command, $resolvedMiddleware, $params, $info); 166 | } 167 | 168 | return; 169 | } 170 | 171 | /** 172 | * Run Route Command; Controller or Closure 173 | * 174 | * @param string|Closure $command 175 | * @param array $params 176 | * 177 | * @return mixed 178 | * @throws Exception 179 | */ 180 | public function runRoute(string|Closure $command, array $params = []): mixed 181 | { 182 | $info = $this->getControllerInfo(); 183 | if (!is_object($command)) { 184 | $invokable = !str_contains($command, '@'); 185 | $class = $command; 186 | if (!$invokable) { 187 | [$class, $method] = explode('@', $command); 188 | } 189 | 190 | $class = str_replace([$info['namespace'], '\\', '.'], ['', '/', '/'], $class); 191 | 192 | $controller = $this->resolveClass($class, $info['path'], $info['namespace']); 193 | if (!$invokable && !method_exists($controller, $method)) { 194 | $this->exception("{$method} method is not found in {$class} class."); 195 | } 196 | 197 | if (property_exists($controller, 'middlewareBefore') && is_array($controller->middlewareBefore)) { 198 | foreach ($controller->middlewareBefore as $middleware) { 199 | $this->beforeAfter($middleware); 200 | } 201 | } 202 | 203 | $response = $this->runMethodWithParams([$controller, (!$invokable ? $method : '__invoke')], $params); 204 | 205 | if (property_exists($controller, 'middlewareAfter') && is_array($controller->middlewareAfter)) { 206 | foreach ($controller->middlewareAfter as $middleware) { 207 | $this->beforeAfter($middleware); 208 | } 209 | } 210 | 211 | return $response; 212 | } 213 | 214 | return $this->runMethodWithParams($command, $params); 215 | } 216 | 217 | /** 218 | * Resolve Controller or Middleware class. 219 | * 220 | * @param string $class 221 | * @param string $path 222 | * @param string $namespace 223 | * 224 | * @return object 225 | * @throws Exception 226 | */ 227 | protected function resolveClass(string $class, string $path, string $namespace): object 228 | { 229 | $class = str_replace([$namespace, '\\'], ['', '/'], $class); 230 | $file = realpath("{$path}/{$class}.php"); 231 | if (!file_exists($file)) { 232 | $this->exception("{$class} class is not found. Please check the file."); 233 | } 234 | 235 | $class = $namespace . str_replace('/', '\\', $class); 236 | if (!class_exists($class)) { 237 | require_once($file); 238 | } 239 | 240 | return new $class(); 241 | } 242 | 243 | /** 244 | * @param array|Closure $function 245 | * @param array $params 246 | * 247 | * @return Response|mixed 248 | * @throws ReflectionException 249 | */ 250 | protected function runMethodWithParams(array|Closure $function, array $params): mixed 251 | { 252 | $reflection = is_array($function) 253 | ? new ReflectionMethod($function[0], $function[1]) 254 | : new ReflectionFunction($function); 255 | $parameters = $this->resolveCallbackParameters($reflection, $params); 256 | $response = call_user_func_array($function, $parameters); 257 | return $this->sendResponse($response); 258 | } 259 | 260 | /** 261 | * @param Reflector $reflection 262 | * @param array $uriParams 263 | * 264 | * @return array 265 | * @throws 266 | */ 267 | protected function resolveCallbackParameters(Reflector $reflection, array $uriParams): array 268 | { 269 | $parameters = []; 270 | foreach ($reflection->getParameters() as $key => $param) { 271 | $class = $param->getType() && !$param->getType()->isBuiltin() 272 | ? new ReflectionClass($param->getType()->getName()) 273 | : null; 274 | if (!is_null($class) && $class->isInstance($this->request)) { 275 | $parameters[] = $this->request; 276 | } elseif (!is_null($class) && $class->isInstance($this->response)) { 277 | $parameters[] = $this->response; 278 | } elseif (!is_null($class)) { 279 | $parameters[] = null; 280 | } else { 281 | if (empty($uriParams)) { 282 | continue; 283 | } 284 | $uriParams = array_reverse($uriParams); 285 | $parameters[] = array_pop($uriParams); 286 | $uriParams = array_reverse($uriParams); 287 | } 288 | } 289 | 290 | return $parameters; 291 | } 292 | 293 | /** 294 | * @param string $command 295 | * @param string $middleware 296 | * @param array $params 297 | * @param array $info 298 | * 299 | * @return bool|void 300 | * @throws ReflectionException 301 | * @throws Exception 302 | */ 303 | protected function runMiddleware(string $command, string $middleware, array $params, array $info) 304 | { 305 | $middlewareMethod = 'handle'; // For now, it's constant. 306 | $controller = $this->resolveClass($middleware, $info['path'], $info['namespace']); 307 | 308 | if (in_array($command, $this->markedMiddlewares)) { 309 | return true; 310 | } 311 | $this->markedMiddlewares[] = $command; 312 | 313 | if (!method_exists($controller, $middlewareMethod)) { 314 | $this->exception("{$middlewareMethod}() method is not found in {$middleware} class."); 315 | } 316 | 317 | $parameters = $this->resolveCallbackParameters(new ReflectionMethod($controller, $middlewareMethod), $params); 318 | $response = call_user_func_array([$controller, $middlewareMethod], $parameters); 319 | if ($response !== true) { 320 | $this->sendResponse($response); 321 | exit; 322 | } 323 | 324 | return true; 325 | } 326 | 327 | /** 328 | * @param string $middleware 329 | * 330 | * @return array|string 331 | */ 332 | protected function resolveMiddleware(string $middleware): array|string 333 | { 334 | $middlewares = $this->middlewares; 335 | if (isset($middlewares['middlewareGroups'][$middleware])) { 336 | return $middlewares['middlewareGroups'][$middleware]; 337 | } 338 | 339 | $name = explode(':', $middleware)[0]; 340 | if (isset($middlewares['routeMiddlewares'][$name])) { 341 | return $middlewares['routeMiddlewares'][$name]; 342 | } 343 | 344 | return $middleware; 345 | } 346 | 347 | /** 348 | * @param $response 349 | * 350 | * @return Response|mixed 351 | */ 352 | public function sendResponse($response): mixed 353 | { 354 | if (is_array($response) || str_contains($this->request->headers->get('Accept') ?? '', 'application/json')) { 355 | $this->response->headers->set('Content-Type', 'application/json'); 356 | return $this->response 357 | ->setContent($response instanceof Response ? $response->getContent() : json_encode($response)) 358 | ->send(); 359 | } 360 | 361 | if (!is_string($response)) { 362 | return $response instanceof Response ? $response->send() : print($response); 363 | } 364 | 365 | return $this->response->setContent($response)->send(); 366 | } 367 | 368 | /** 369 | * Throw new Exception for Router Error 370 | * 371 | * @param string $message 372 | * @param int $statusCode 373 | * 374 | * @throws Exception 375 | */ 376 | protected function exception(string $message = '', int $statusCode = Response::HTTP_INTERNAL_SERVER_ERROR) 377 | { 378 | throw new RouterException($message, $statusCode); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/RouterException.php: -------------------------------------------------------------------------------- 1 | request = $request; 28 | $this->response = $response; 29 | } 30 | 31 | /** 32 | * @return Request 33 | */ 34 | public function symfonyRequest(): Request 35 | { 36 | return $this->request; 37 | } 38 | 39 | /** 40 | * @return Response 41 | */ 42 | public function symfonyResponse(): Response 43 | { 44 | return $this->response; 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function validMethods(): string 51 | { 52 | return $this->validMethods; 53 | } 54 | 55 | /** 56 | * Request method validation 57 | * 58 | * @param string $data 59 | * @param string $method 60 | * 61 | * @return bool 62 | */ 63 | public function validMethod(string $data, string $method): bool 64 | { 65 | $valid = false; 66 | if (str_contains($data, '|')) { 67 | foreach (explode('|', $data) as $value) { 68 | $valid = $this->checkMethods($value, $method); 69 | if ($valid) { 70 | break; 71 | } 72 | } 73 | } else { 74 | $valid = $this->checkMethods($data, $method); 75 | } 76 | 77 | return $valid; 78 | } 79 | 80 | /** 81 | * Get the request method used, taking overrides into account 82 | * 83 | * @return string 84 | */ 85 | public function getMethod(): string 86 | { 87 | $method = $this->request->getMethod(); 88 | $formMethod = $this->request->request->get('_method'); 89 | if (!empty($formMethod)) { 90 | $method = $formMethod; 91 | } 92 | 93 | return strtoupper($method); 94 | } 95 | 96 | /** 97 | * check method valid 98 | * 99 | * @param string $value 100 | * @param string $method 101 | * 102 | * @return bool 103 | */ 104 | protected function checkMethods(string $value, string $method): bool 105 | { 106 | if (in_array($value, explode('|', $this->validMethods))) { 107 | if ($this->request->isXmlHttpRequest() && $value === 'AJAX') { 108 | return true; 109 | } 110 | 111 | if ($this->request->isXmlHttpRequest() && str_starts_with($value, 'X') 112 | && $method === ltrim($value, 'X')) { 113 | return true; 114 | } 115 | 116 | if (in_array($value, [$method, 'ANY'])) { 117 | return true; 118 | } 119 | } 120 | 121 | return false; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Example/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteBase /tests/Example 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteRule ^ index.php [QSA,L] 6 | -------------------------------------------------------------------------------- /tests/Example/Controllers/FooController.php: -------------------------------------------------------------------------------- 1 | setContent('Foo in TestController!'); 18 | 19 | return $response; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Example/Middlewares/TestMiddleware.php: -------------------------------------------------------------------------------- 1 | true, 7 | 'paths' => [ 8 | 'controllers' => __DIR__ . '/Controllers', 9 | 'middlewares' => __DIR__ . '/Middlewares', 10 | ], 11 | 'namespaces' => [ 12 | 'controllers' => 'Buki\\Tests\\Example\\Controllers', 13 | 'middlewares' => 'Buki\\Tests\\Example\\Middlewares', 14 | ], 15 | 'base_folder' => __DIR__, 16 | 'main_method' => 'main', 17 | ]; 18 | 19 | $router = new \Buki\Router\Router($params); 20 | 21 | $router->get('/', function() { 22 | return 'Hello World!'; 23 | }, ['before' => 'TestMiddleware:burak,30']); 24 | 25 | $router->get('/test', 'TestController@main'); 26 | $router->get('/test2', ['TestController', 'main']); 27 | $router->get('/invoke', 'FooController'); 28 | 29 | $router->controller('/controller', 'TestController'); 30 | 31 | $router->run(); 32 | -------------------------------------------------------------------------------- /tests/Example/travis-ci-apache: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot %TRAVIS_BUILD_DIR% 3 | 4 | 5 | Options FollowSymLinks MultiViews ExecCGI 6 | AllowOverride All 7 | Require all granted 8 | 9 | 10 | # Wire up Apache to use Travis CI's php-fpm. 11 | 12 | AddHandler php-fcgi .php 13 | Action php-fcgi /php-fcgi 14 | Alias /php-fcgi /usr/lib/cgi-bin/php-fcgi 15 | FastCgiExternalServer /usr/lib/cgi-bin/php-fcgi -host 127.0.0.1:9000 -pass-header Authorization 16 | 17 | Require all granted 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/Example/www.conf.default: -------------------------------------------------------------------------------- 1 | ; Start a new pool named 'www'. 2 | ; the variable $pool can we used in any directive and will be replaced by the 3 | ; pool name ('www' here) 4 | [www] 5 | 6 | ; Per pool prefix 7 | ; It only applies on the following directives: 8 | ; - 'slowlog' 9 | ; - 'listen' (unixsocket) 10 | ; - 'chroot' 11 | ; - 'chdir' 12 | ; - 'php_values' 13 | ; - 'php_admin_values' 14 | ; When not set, the global prefix (or /usr) applies instead. 15 | ; Note: This directive can also be relative to the global prefix. 16 | ; Default Value: none 17 | ;prefix = /path/to/pools/$pool 18 | 19 | ; Unix user/group of processes 20 | ; Note: The user is mandatory. If the group is not set, the default user's group 21 | ; will be used. 22 | user = www-data 23 | group = www-data 24 | 25 | ; The address on which to accept FastCGI requests. 26 | ; Valid syntaxes are: 27 | ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific address on 28 | ; a specific port; 29 | ; 'port' - to listen on a TCP socket to all addresses on a 30 | ; specific port; 31 | ; '/path/to/unix/socket' - to listen on a unix socket. 32 | ; Note: This value is mandatory. 33 | listen = 127.0.0.1:9000 34 | 35 | ; Set listen(2) backlog. A value of '-1' means unlimited. 36 | ; Default Value: 128 (-1 on FreeBSD and OpenBSD) 37 | ;listen.backlog = -1 38 | 39 | ; Set permissions for unix socket, if one is used. In Linux, read/write 40 | ; permissions must be set in order to allow connections from a web server. Many 41 | ; BSD-derived systems allow connections regardless of permissions. 42 | ; Default Values: user and group are set as the running user 43 | ; mode is set to 0666 44 | ;listen.owner = www-data 45 | ;listen.group = www-data 46 | ;listen.mode = 0666 47 | 48 | ; List of ipv4 addresses of FastCGI clients which are allowed to connect. 49 | ; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original 50 | ; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address 51 | ; must be separated by a comma. If this value is left blank, connections will be 52 | ; accepted from any ip address. 53 | ; Default Value: any 54 | ;listen.allowed_clients = 127.0.0.1 55 | 56 | ; Choose how the process manager will control the number of child processes. 57 | ; Possible Values: 58 | ; static - a fixed number (pm.max_children) of child processes; 59 | ; dynamic - the number of child processes are set dynamically based on the 60 | ; following directives. With this process management, there will be 61 | ; always at least 1 children. 62 | ; pm.max_children - the maximum number of children that can 63 | ; be alive at the same time. 64 | ; pm.start_servers - the number of children created on startup. 65 | ; pm.min_spare_servers - the minimum number of children in 'idle' 66 | ; state (waiting to process). If the number 67 | ; of 'idle' processes is less than this 68 | ; number then some children will be created. 69 | ; pm.max_spare_servers - the maximum number of children in 'idle' 70 | ; state (waiting to process). If the number 71 | ; of 'idle' processes is greater than this 72 | ; number then some children will be killed. 73 | ; ondemand - no children are created at startup. Children will be forked when 74 | ; new requests will connect. The following parameter are used: 75 | ; pm.max_children - the maximum number of children that 76 | ; can be alive at the same time. 77 | ; pm.process_idle_timeout - The number of seconds after which 78 | ; an idle process will be killed. 79 | ; Note: This value is mandatory. 80 | pm = dynamic 81 | 82 | ; The number of child processes to be created when pm is set to 'static' and the 83 | ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. 84 | ; This value sets the limit on the number of simultaneous requests that will be 85 | ; served. Equivalent to the ApacheMaxClients directive with mpm_prefork. 86 | ; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP 87 | ; CGI. The below defaults are based on a server without much resources. Don't 88 | ; forget to tweak pm.* to fit your needs. 89 | ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' 90 | ; Note: This value is mandatory. 91 | pm.max_children = 10 92 | 93 | ; The number of child processes created on startup. 94 | ; Note: Used only when pm is set to 'dynamic' 95 | ; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2 96 | pm.start_servers = 4 97 | 98 | ; The desired minimum number of idle server processes. 99 | ; Note: Used only when pm is set to 'dynamic' 100 | ; Note: Mandatory when pm is set to 'dynamic' 101 | pm.min_spare_servers = 2 102 | 103 | ; The desired maximum number of idle server processes. 104 | ; Note: Used only when pm is set to 'dynamic' 105 | ; Note: Mandatory when pm is set to 'dynamic' 106 | pm.max_spare_servers = 6 107 | 108 | ; The number of seconds after which an idle process will be killed. 109 | ; Note: Used only when pm is set to 'ondemand' 110 | ; Default Value: 10s 111 | ;pm.process_idle_timeout = 10s; 112 | 113 | ; The number of requests each child process should execute before respawning. 114 | ; This can be useful to work around memory leaks in 3rd party libraries. For 115 | ; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. 116 | ; Default Value: 0 117 | ;pm.max_requests = 500 118 | 119 | ; The URI to view the FPM status page. If this value is not set, no URI will be 120 | ; recognized as a status page. It shows the following informations: 121 | ; pool - the name of the pool; 122 | ; process manager - static, dynamic or ondemand; 123 | ; start time - the date and time FPM has started; 124 | ; start since - number of seconds since FPM has started; 125 | ; accepted conn - the number of request accepted by the pool; 126 | ; listen queue - the number of request in the queue of pending 127 | ; connections (see backlog in listen(2)); 128 | ; max listen queue - the maximum number of requests in the queue 129 | ; of pending connections since FPM has started; 130 | ; listen queue len - the size of the socket queue of pending connections; 131 | ; idle processes - the number of idle processes; 132 | ; active processes - the number of active processes; 133 | ; total processes - the number of idle + active processes; 134 | ; max active processes - the maximum number of active processes since FPM 135 | ; has started; 136 | ; max children reached - number of times, the process limit has been reached, 137 | ; when pm tries to start more children (works only for 138 | ; pm 'dynamic' and 'ondemand'); 139 | ; Value are updated in real time. 140 | ; Example output: 141 | ; pool: www 142 | ; process manager: static 143 | ; start time: 01/Jul/2011:17:53:49 +0200 144 | ; start since: 62636 145 | ; accepted conn: 190460 146 | ; listen queue: 0 147 | ; max listen queue: 1 148 | ; listen queue len: 42 149 | ; idle processes: 4 150 | ; active processes: 11 151 | ; total processes: 15 152 | ; max active processes: 12 153 | ; max children reached: 0 154 | ; 155 | ; By default the status page output is formatted as text/plain. Passing either 156 | ; 'html', 'xml' or 'json' in the query string will return the corresponding 157 | ; output syntax. Example: 158 | ; http://www.foo.bar/status 159 | ; http://www.foo.bar/status?json 160 | ; http://www.foo.bar/status?html 161 | ; http://www.foo.bar/status?xml 162 | ; 163 | ; By default the status page only outputs short status. Passing 'full' in the 164 | ; query string will also return status for each pool process. 165 | ; Example: 166 | ; http://www.foo.bar/status?full 167 | ; http://www.foo.bar/status?json&full 168 | ; http://www.foo.bar/status?html&full 169 | ; http://www.foo.bar/status?xml&full 170 | ; The Full status returns for each process: 171 | ; pid - the PID of the process; 172 | ; state - the state of the process (Idle, Running, ...); 173 | ; start time - the date and time the process has started; 174 | ; start since - the number of seconds since the process has started; 175 | ; requests - the number of requests the process has served; 176 | ; request duration - the duration in µs of the requests; 177 | ; request method - the request method (GET, POST, ...); 178 | ; request URI - the request URI with the query string; 179 | ; content length - the content length of the request (only with POST); 180 | ; user - the user (PHP_AUTH_USER) (or '-' if not set); 181 | ; script - the main script called (or '-' if not set); 182 | ; last request cpu - the %cpu the last request consumed 183 | ; it's always 0 if the process is not in Idle state 184 | ; because CPU calculation is done when the request 185 | ; processing has terminated; 186 | ; last request memory - the max amount of memory the last request consumed 187 | ; it's always 0 if the process is not in Idle state 188 | ; because memory calculation is done when the request 189 | ; processing has terminated; 190 | ; If the process is in Idle state, then informations are related to the 191 | ; last request the process has served. Otherwise informations are related to 192 | ; the current request being served. 193 | ; Example output: 194 | ; ************************ 195 | ; pid: 31330 196 | ; state: Running 197 | ; start time: 01/Jul/2011:17:53:49 +0200 198 | ; start since: 63087 199 | ; requests: 12808 200 | ; request duration: 1250261 201 | ; request method: GET 202 | ; request URI: /test_mem.php?N=10000 203 | ; content length: 0 204 | ; user: - 205 | ; script: /home/fat/web/docs/php/test_mem.php 206 | ; last request cpu: 0.00 207 | ; last request memory: 0 208 | ; 209 | ; Note: There is a real-time FPM status monitoring sample web page available 210 | ; It's available in: ${prefix}/share/fpm/status.html 211 | ; 212 | ; Note: The value must start with a leading slash (/). The value can be 213 | ; anything, but it may not be a good idea to use the .php extension or it 214 | ; may conflict with a real PHP file. 215 | ; Default Value: not set 216 | ;pm.status_path = /status 217 | 218 | ; The ping URI to call the monitoring page of FPM. If this value is not set, no 219 | ; URI will be recognized as a ping page. This could be used to test from outside 220 | ; that FPM is alive and responding, or to 221 | ; - create a graph of FPM availability (rrd or such); 222 | ; - remove a server from a group if it is not responding (load balancing); 223 | ; - trigger alerts for the operating team (24/7). 224 | ; Note: The value must start with a leading slash (/). The value can be 225 | ; anything, but it may not be a good idea to use the .php extension or it 226 | ; may conflict with a real PHP file. 227 | ; Default Value: not set 228 | ;ping.path = /ping 229 | 230 | ; This directive may be used to customize the response of a ping request. The 231 | ; response is formatted as text/plain with a 200 response code. 232 | ; Default Value: pong 233 | ;ping.response = pong 234 | 235 | ; The access log file 236 | ; Default: not set 237 | ;access.log = log/$pool.access.log 238 | 239 | ; The access log format. 240 | ; The following syntax is allowed 241 | ; %%: the '%' character 242 | ; %C: %CPU used by the request 243 | ; it can accept the following format: 244 | ; - %{user}C for user CPU only 245 | ; - %{system}C for system CPU only 246 | ; - %{total}C for user + system CPU (default) 247 | ; %d: time taken to serve the request 248 | ; it can accept the following format: 249 | ; - %{seconds}d (default) 250 | ; - %{miliseconds}d 251 | ; - %{mili}d 252 | ; - %{microseconds}d 253 | ; - %{micro}d 254 | ; %e: an environment variable (same as $_ENV or $_SERVER) 255 | ; it must be associated with embraces to specify the name of the env 256 | ; variable. Some exemples: 257 | ; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e 258 | ; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e 259 | ; %f: script filename 260 | ; %l: content-length of the request (for POST request only) 261 | ; %m: request method 262 | ; %M: peak of memory allocated by PHP 263 | ; it can accept the following format: 264 | ; - %{bytes}M (default) 265 | ; - %{kilobytes}M 266 | ; - %{kilo}M 267 | ; - %{megabytes}M 268 | ; - %{mega}M 269 | ; %n: pool name 270 | ; %o: ouput header 271 | ; it must be associated with embraces to specify the name of the header: 272 | ; - %{Content-Type}o 273 | ; - %{X-Powered-By}o 274 | ; - %{Transfert-Encoding}o 275 | ; - .... 276 | ; %p: PID of the child that serviced the request 277 | ; %P: PID of the parent of the child that serviced the request 278 | ; %q: the query string 279 | ; %Q: the '?' character if query string exists 280 | ; %r: the request URI (without the query string, see %q and %Q) 281 | ; %R: remote IP address 282 | ; %s: status (response code) 283 | ; %t: server time the request was received 284 | ; it can accept a strftime(3) format: 285 | ; %d/%b/%Y:%H:%M:%S %z (default) 286 | ; %T: time the log has been written (the request has finished) 287 | ; it can accept a strftime(3) format: 288 | ; %d/%b/%Y:%H:%M:%S %z (default) 289 | ; %u: remote user 290 | ; 291 | ; Default: "%R - %u %t \"%m %r\" %s" 292 | ;access.format = %R - %u %t "%m %r%Q%q" %s %f %{mili}d %{kilo}M %C%% 293 | 294 | ; The log file for slow requests 295 | ; Default Value: not set 296 | ; Note: slowlog is mandatory if request_slowlog_timeout is set 297 | ;slowlog = log/$pool.log.slow 298 | 299 | ; The timeout for serving a single request after which a PHP backtrace will be 300 | ; dumped to the 'slowlog' file. A value of '0s' means 'off'. 301 | ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) 302 | ; Default Value: 0 303 | ;request_slowlog_timeout = 0 304 | 305 | ; The timeout for serving a single request after which the worker process will 306 | ; be killed. This option should be used when the 'max_execution_time' ini option 307 | ; does not stop script execution for some reason. A value of '0' means 'off'. 308 | ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) 309 | ; Default Value: 0 310 | ;request_terminate_timeout = 0 311 | 312 | ; Set open file descriptor rlimit. 313 | ; Default Value: system defined value 314 | ;rlimit_files = 1024 315 | 316 | ; Set max core size rlimit. 317 | ; Possible Values: 'unlimited' or an integer greater or equal to 0 318 | ; Default Value: system defined value 319 | ;rlimit_core = 0 320 | 321 | ; Chroot to this directory at the start. This value must be defined as an 322 | ; absolute path. When this value is not set, chroot is not used. 323 | ; Note: you can prefix with '$prefix' to chroot to the pool prefix or one 324 | ; of its subdirectories. If the pool prefix is not set, the global prefix 325 | ; will be used instead. 326 | ; Note: chrooting is a great security feature and should be used whenever 327 | ; possible. However, all PHP paths will be relative to the chroot 328 | ; (error_log, sessions.save_path, ...). 329 | ; Default Value: not set 330 | ;chroot = 331 | 332 | ; Chdir to this directory at the start. 333 | ; Note: relative path can be used. 334 | ; Default Value: current directory or / when chroot 335 | chdir = / 336 | 337 | ; Redirect worker stdout and stderr into main error log. If not set, stdout and 338 | ; stderr will be redirected to /dev/null according to FastCGI specs. 339 | ; Note: on highloaded environement, this can cause some delay in the page 340 | ; process time (several ms). 341 | ; Default Value: no 342 | ;catch_workers_output = yes 343 | 344 | ; Limits the extensions of the main script FPM will allow to parse. This can 345 | ; prevent configuration mistakes on the web server side. You should only limit 346 | ; FPM to .php extensions to prevent malicious users to use other extensions to 347 | ; exectute php code. 348 | ; Note: set an empty value to allow all extensions. 349 | ; Default Value: .php 350 | ;security.limit_extensions = .php .php3 .php4 .php5 351 | 352 | ; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from 353 | ; the current environment. 354 | ; Default Value: clean env 355 | ;env[HOSTNAME] = $HOSTNAME 356 | ;env[PATH] = /usr/local/bin:/usr/bin:/bin 357 | ;env[TMP] = /tmp 358 | ;env[TMPDIR] = /tmp 359 | ;env[TEMP] = /tmp 360 | 361 | ; Additional php.ini defines, specific to this pool of workers. These settings 362 | ; overwrite the values previously defined in the php.ini. The directives are the 363 | ; same as the PHP SAPI: 364 | ; php_value/php_flag - you can set classic ini defines which can 365 | ; be overwritten from PHP call 'ini_set'. 366 | ; php_admin_value/php_admin_flag - these directives won't be overwritten by 367 | ; PHP call 'ini_set' 368 | ; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no. 369 | 370 | ; Defining 'extension' will load the corresponding shared extension from 371 | ; extension_dir. Defining 'disable_functions' or 'disable_classes' will not 372 | ; overwrite previously defined php.ini values, but will append the new value 373 | ; instead. 374 | 375 | ; Note: path INI options can be relative and will be expanded with the prefix 376 | ; (pool, global or /usr) 377 | 378 | ; Default Value: nothing is defined by default except the values in php.ini and 379 | ; specified at startup with the -d argument 380 | ;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com 381 | ;php_flag[display_errors] = off 382 | ;php_admin_value[error_log] = /var/log/fpm-php.www.log 383 | ;php_admin_flag[log_errors] = on 384 | ;php_admin_value[memory_limit] = 32M 385 | -------------------------------------------------------------------------------- /tests/RouterTest.php: -------------------------------------------------------------------------------- 1 | request = Request::createFromGlobals(); 19 | $this->router = new Router( 20 | [], 21 | $this->request 22 | ); 23 | 24 | // Clear SCRIPT_NAME because bramus/router tries to guess the subfolder the script is run in 25 | $this->request->server->set('SCRIPT_NAME', '/index.php'); 26 | $this->request->server->set('SCRIPT_FILENAME', '/index.php'); 27 | 28 | // Default request method to GET 29 | $this->request->server->set('REQUEST_METHOD', 'GET'); 30 | 31 | // Default SERVER_PROTOCOL method to HTTP/1.1 32 | $this->request->server->set('SERVER_PROTOCOL', 'HTTP/1.1'); 33 | } 34 | 35 | protected function tearDown(): void 36 | { 37 | // nothing 38 | } 39 | 40 | public function testInit() 41 | { 42 | $this->assertInstanceOf('\Buki\Router\Router', $this->router); 43 | } 44 | 45 | public function testRouteCount() 46 | { 47 | $this->router->get('/', 'HomeController@main'); 48 | $this->router->get('/contact', 'HomeController@contact'); 49 | $this->router->get('/about', 'HomeController@about'); 50 | 51 | $this->assertCount(3, $this->router->getRoutes(), "doesn't contains 2 routes"); 52 | } 53 | 54 | public function testRequestMethods() 55 | { 56 | $this->router->get('/get', function () { 57 | return 'get'; 58 | }); 59 | $this->router->post('/post', function () { 60 | return 'post'; 61 | }); 62 | $this->router->put('/put', function () { 63 | return 'put'; 64 | }); 65 | $this->router->patch('/', function () { 66 | return 'patch'; 67 | }); 68 | $this->router->delete('/', function () { 69 | return 'delete'; 70 | }); 71 | $this->router->options('/', function () { 72 | return 'options'; 73 | }); 74 | 75 | // Test GET 76 | ob_start(); 77 | $this->request->server->set('REQUEST_URI', '/get'); 78 | $this->request->server->set('REQUEST_METHOD', 'GET'); 79 | $this->router->run(); 80 | $this->assertEquals('get', ob_get_contents()); 81 | 82 | // Cleanup 83 | ob_end_clean(); 84 | } 85 | 86 | public function testDynamicRequestUri() 87 | { 88 | $this->router->get('/test/:id', function (int $id) { 89 | return "result: {$id}"; 90 | }); 91 | 92 | $this->router->get('/test/:string', function (string $username) { 93 | return "result: {$username}"; 94 | }); 95 | 96 | $this->router->get('/test/:uuid', function (string $uuid) { 97 | return "result: ce3a3f47-b950-4e34-97ee-fa5f127d4564"; 98 | }); 99 | 100 | $this->router->get('/test/:date', function (string $date) { 101 | return "result: 1938-11-10"; 102 | }); 103 | 104 | $this->request->server->set('REQUEST_METHOD', 'GET'); 105 | 106 | ob_start(); 107 | $this->request->server->set('REQUEST_URI', '/test/10'); 108 | $this->router->run(); 109 | $this->assertEquals('result: 10', ob_get_contents()); 110 | 111 | ob_clean(); 112 | $this->request->server->set('REQUEST_URI', '/test/izniburak'); 113 | $this->router->run(); 114 | $this->assertEquals('result: izniburak', ob_get_contents()); 115 | 116 | ob_clean(); 117 | $this->request->server->set('REQUEST_URI', '/test/ce3a3f47-b950-4e34-97ee-fa5f127d4564'); 118 | $this->router->run(); 119 | $this->assertEquals('result: ce3a3f47-b950-4e34-97ee-fa5f127d4564', ob_get_contents()); 120 | 121 | ob_clean(); 122 | $this->request->server->set('REQUEST_URI', '/test/1938-11-10'); 123 | $this->router->run(); 124 | $this->assertEquals('result: 1938-11-10', ob_get_contents()); 125 | 126 | // Cleanup 127 | ob_end_clean(); 128 | } 129 | 130 | public function testPostRequestMethod() 131 | { 132 | $this->router->post('/test', function () { 133 | return "success"; 134 | }); 135 | 136 | $this->request->server->set('REQUEST_METHOD', 'POST'); 137 | 138 | ob_start(); 139 | $this->request->server->set('REQUEST_URI', '/test'); 140 | $this->router->run(); 141 | $this->assertEquals('success', ob_get_contents()); 142 | 143 | // Cleanup 144 | ob_end_clean(); 145 | } 146 | 147 | public function testPutRequestMethod() 148 | { 149 | $this->router->put('/test', function () { 150 | return "success"; 151 | }); 152 | 153 | $this->request->server->set('REQUEST_METHOD', 'PUT'); 154 | 155 | ob_start(); 156 | $this->request->server->set('REQUEST_URI', '/test'); 157 | $this->router->run(); 158 | $this->assertEquals('success', ob_get_contents()); 159 | 160 | // Cleanup 161 | ob_end_clean(); 162 | } 163 | 164 | public function testDeleteRequestMethod() 165 | { 166 | $this->router->delete('/test', function () { 167 | return "success"; 168 | }); 169 | 170 | $this->request->server->set('REQUEST_METHOD', 'DELETE'); 171 | 172 | ob_start(); 173 | $this->request->server->set('REQUEST_URI', '/test'); 174 | $this->router->run(); 175 | $this->assertEquals('success', ob_get_contents()); 176 | 177 | // Cleanup 178 | ob_end_clean(); 179 | } 180 | 181 | public function testPatchRequestMethod() 182 | { 183 | $this->router->patch('/test', function () { 184 | return "success"; 185 | }); 186 | 187 | $this->request->server->set('REQUEST_METHOD', 'PATCH'); 188 | 189 | ob_start(); 190 | $this->request->server->set('REQUEST_URI', '/test'); 191 | $this->router->run(); 192 | $this->assertEquals('success', ob_get_contents()); 193 | 194 | // Cleanup 195 | ob_end_clean(); 196 | } 197 | 198 | public function testOptionsRequestMethod() 199 | { 200 | $this->router->options('/test', function () { 201 | return "success"; 202 | }); 203 | 204 | $this->request->server->set('REQUEST_METHOD', 'OPTIONS'); 205 | 206 | ob_start(); 207 | $this->request->server->set('REQUEST_URI', '/test'); 208 | $this->router->run(); 209 | $this->assertEquals('success', ob_get_contents()); 210 | 211 | // Cleanup 212 | ob_end_clean(); 213 | } 214 | 215 | public function testXGetRequestMethod() 216 | { 217 | $this->router->xget('/test', function () { 218 | return "success"; 219 | }); 220 | 221 | $this->request->server->set('REQUEST_METHOD', 'XGET'); 222 | $this->request->server->set('X-Requested-With', 'XMLHttpRequest'); 223 | 224 | ob_start(); 225 | $this->request->server->set('REQUEST_URI', '/test'); 226 | $this->router->run(); 227 | $this->assertEquals('success', ob_get_contents()); 228 | 229 | // Cleanup 230 | ob_end_clean(); 231 | } 232 | 233 | public function testXPostRequestMethod() 234 | { 235 | $this->router->xpost('/test', function () { 236 | return "success"; 237 | }); 238 | 239 | $this->request->server->set('REQUEST_METHOD', 'XPOST'); 240 | $this->request->server->set('X-Requested-With', 'XMLHttpRequest'); 241 | 242 | ob_start(); 243 | $this->request->server->set('REQUEST_URI', '/test'); 244 | $this->router->run(); 245 | $this->assertEquals('success', ob_get_contents()); 246 | 247 | // Cleanup 248 | ob_end_clean(); 249 | } 250 | } 251 | --------------------------------------------------------------------------------