├── .DS_Store ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .php-cs-fixer.cache ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── rector.php ├── src ├── .github │ └── workflows │ │ └── tests.yml ├── RouteCollection.php ├── Router.php └── RouterEngine.php └── tests ├── Demo ├── App │ ├── Main.php │ └── Test.php ├── Appo │ └── Hi.php ├── Bye.php ├── Hello.php ├── Main.php ├── Param.php └── Test.php ├── Pest.php └── Unit ├── ManualRouteTest.php ├── RouterCollectionTest.php ├── RouterEngineTest.php └── RouterTest.php /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrawler-labs/router/f801eff3f786cc82f187752d67c2e81de13fbbf9/.DS_Store -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "composer" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request,workflow_dispatch] 3 | jobs: 4 | router-tests: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | operating-system: ['ubuntu-latest'] 9 | php-versions: ['8. 3'] 10 | phpunit-versions: ['latest'] 11 | include: 12 | - operating-system: 'ubuntu-latest' 13 | php-versions: '8.3' 14 | steps: 15 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 16 | - uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: '8.3' 19 | - name: Update Composer 20 | run: sudo composer self-update --no-interaction 21 | - name: Run Composer Install 22 | run: composer install --no-interaction 23 | - name: run tests 24 | run: vendor/bin/pest --coverage-clover ./clover.xml 25 | - name: run static analysis 26 | run: vendor/bin/phpstan analyse src --level 5 27 | - name: Upload to Codecov 28 | uses: codecov/codecov-action@v4 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/private/ 2 | /vendor -------------------------------------------------------------------------------- /.php-cs-fixer.cache: -------------------------------------------------------------------------------- 1 | {"php":"8.3.17","version":"3.68.5","indent":" ","lineEnding":"\n","rules":{"align_multiline_comment":true,"backtick_to_shell_exec":true,"binary_operator_spaces":true,"blank_line_before_statement":{"statements":["return"]},"braces_position":{"allow_single_line_anonymous_functions":true,"allow_single_line_empty_anonymous_classes":true},"class_attributes_separation":{"elements":{"method":"one"}},"class_definition":{"single_line":true},"class_reference_name_casing":true,"clean_namespace":true,"concat_space":true,"declare_parentheses":true,"echo_tag_syntax":true,"empty_loop_body":{"style":"braces"},"empty_loop_condition":true,"fully_qualified_strict_types":true,"function_declaration":true,"general_phpdoc_tag_rename":{"replacements":{"inheritDocs":"inheritDoc"}},"global_namespace_import":{"import_classes":false,"import_constants":false,"import_functions":false},"include":true,"increment_style":true,"integer_literal_case":true,"lambda_not_used_import":true,"linebreak_after_opening_tag":true,"magic_constant_casing":true,"magic_method_casing":true,"method_argument_space":{"on_multiline":"ignore"},"native_function_casing":true,"native_type_declaration_casing":true,"no_alias_language_construct_call":true,"no_alternative_syntax":true,"no_binary_string":true,"no_blank_lines_after_phpdoc":true,"no_empty_comment":true,"no_empty_phpdoc":true,"no_empty_statement":true,"no_extra_blank_lines":{"tokens":["attribute","case","continue","curly_brace_block","default","extra","parenthesis_brace_block","square_brace_block","switch","throw","use"]},"no_leading_namespace_whitespace":true,"no_mixed_echo_print":true,"no_multiline_whitespace_around_double_arrow":true,"no_null_property_initialization":true,"no_short_bool_cast":true,"no_singleline_whitespace_before_semicolons":true,"no_spaces_around_offset":true,"no_superfluous_phpdoc_tags":{"allow_hidden_params":true,"remove_inheritdoc":true},"no_trailing_comma_in_singleline":true,"no_unneeded_braces":{"namespaces":true},"no_unneeded_control_parentheses":{"statements":["break","clone","continue","echo_print","others","return","switch_case","yield","yield_from"]},"no_unneeded_import_alias":true,"no_unset_cast":true,"no_unused_imports":true,"no_useless_concat_operator":true,"no_useless_nullsafe_operator":true,"no_whitespace_before_comma_in_array":true,"normalize_index_brace":true,"nullable_type_declaration":true,"nullable_type_declaration_for_default_null_value":true,"object_operator_without_whitespace":true,"operator_linebreak":{"only_booleans":true},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"alpha"},"ordered_types":{"null_adjustment":"always_last","sort_algorithm":"none"},"php_unit_fqcn_annotation":true,"php_unit_method_casing":true,"phpdoc_align":true,"phpdoc_annotation_without_dot":true,"phpdoc_indent":true,"phpdoc_inline_tag_normalizer":true,"phpdoc_no_access":true,"phpdoc_no_alias_tag":true,"phpdoc_no_package":true,"phpdoc_no_useless_inheritdoc":true,"phpdoc_order":{"order":["param","return","throws"]},"phpdoc_return_self_reference":true,"phpdoc_scalar":true,"phpdoc_separation":{"groups":[["Annotation","NamedArgumentConstructor","Target"],["author","copyright","license"],["category","package","subpackage"],["property","property-read","property-write"],["deprecated","link","see","since"]]},"phpdoc_single_line_var_spacing":true,"phpdoc_summary":true,"phpdoc_tag_type":{"tags":{"inheritDoc":"inline"}},"phpdoc_to_comment":true,"phpdoc_trim":true,"phpdoc_trim_consecutive_blank_line_separation":true,"phpdoc_types":true,"phpdoc_types_order":{"null_adjustment":"always_last","sort_algorithm":"none"},"phpdoc_var_without_name":true,"semicolon_after_instruction":true,"simple_to_complex_string_variable":true,"single_class_element_per_statement":true,"single_import_per_statement":true,"single_line_comment_spacing":true,"single_line_comment_style":{"comment_types":["hash"]},"single_line_throw":true,"single_quote":true,"single_space_around_construct":true,"space_after_semicolon":{"remove_in_empty_for_expressions":true},"standardize_increment":true,"standardize_not_equals":true,"statement_indentation":{"stick_comment_to_next_continuous_control_statement":true},"switch_continue_to_break":true,"trailing_comma_in_multiline":{"after_heredoc":true,"elements":["array_destructuring","arrays","match","parameters"]},"trim_array_spaces":true,"type_declaration_spaces":true,"types_spaces":true,"unary_operator_spaces":true,"whitespace_after_comma_in_array":true,"yoda_style":true,"array_indentation":true,"array_syntax":true,"cast_spaces":true,"new_with_parentheses":{"anonymous_class":false},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"no_blank_lines_after_class_opening":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"return_type_declaration":true,"short_scalar_cast":true,"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_line_after_imports":true,"spaces_inside_parentheses":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"src\/RouterEngine.php":"2a985df07328dadd7b0ebe98d1b7b7a2","src\/RouteCollection.php":"7e45144fce71de0fe2b1453cfb6f6ad6","src\/Router.php":"741188a7e30c3a4f7e34f7e1654b9370"}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Corpusvision Technologies Pvt Ltd 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Scrawler Router

4 | 5 | GitHub Workflow Status 6 | 7 | [![Codecov](https://img.shields.io/codecov/c/gh/scrawler-labs/router?style=flat-square)](https://app.codecov.io/gh/scrawler-labs/router) 8 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/quality/g/scrawler-labs/router?style=flat-square)](https://scrutinizer-ci.com/g/scrawler-labs/router/?branch=main) 9 | PHPStan Enabled 10 | [![Packagist Version](https://img.shields.io/packagist/v/scrawler/router?style=flat-square)](https://packagist.org/packages/scrawler/router) 11 | [![Packagist Downloads](https://img.shields.io/packagist/dt/scrawler/router?style=flat-square)](https://packagist.org/packages/scrawler/router) 12 | [![Packagist License](https://img.shields.io/packagist/l/scrawler/router?style=flat-square)](https://packagist.org/packages/scrawler/router) 13 |

14 | 15 | 16 | 🔥An Fully Automatic, Framework independent, RESTful PHP Router component🔥
17 | 🇮🇳 Made in India 🇮🇳 18 |
19 | 20 | ![Demo](http://g.recordit.co/lvQba4mnyB.gif) 21 | 22 | 23 | Complete docs can be found [here](https://component.scrawlerlabs.com/router/) 24 | 25 | ## 🤔 Why use Scrawler Router? 26 | - Fully automatic, you dont need to define single manual route. 27 | - Support manual route defination for your edge use case. 28 | - No configrations , works out of the box with any php project. 29 | - Stable and well tested. 30 | - Saves lot of time while building RESTful applications 31 |

32 | 33 | ## 💻 Installation 34 | You can install Scrawler Router via Composer. If you don't have composer installed , you can download composer from [here](https://getcomposer.org/download/) 35 | 36 | ```sh 37 | composer require scrawler/router 38 | ``` 39 | 40 | ## ✨ Setup 41 | 42 | Note 4.x release changes the way router handles request and response, if you still wanna continue using old way with symfony components goto [3.x branch](https://github.com/scrawler-labs/router/tree/3.x) 43 | 44 | ```php 45 | register($dir,$namespace); 55 | 56 | /** 57 | * you can now also enable route caching by passing your own PSR 16 implementation 58 | * $cache = new Psr\SimpleCache\CacheInterface(); 59 | * $router->enableCache($cache); 60 | **/ 61 | 62 | // Fetch method and URI from somewhere 63 | $httpMethod = $_SERVER['REQUEST_METHOD']; 64 | $uri = $_SERVER['REQUEST_URI']; 65 | 66 | // Strip query string (?foo=bar) and decode URI 67 | if (false !== $pos = strpos($uri, '?')) { 68 | $uri = substr($uri, 0, $pos); 69 | } 70 | $uri = rawurldecode($uri); 71 | 72 | //Dispatch route and get back the response 73 | [$status,$handler,$args,$debug] = $router->dispatch($httpMethod,$uri); 74 | switch ($status){ 75 | case \Scrawler\Router\Router::NOT_FOUND: 76 | //handle 404 error 77 | // $debug contains extra debug info useful to check failure in automatic routing 78 | break; 79 | case \Scrawler\Router\Router::METHOD_NOT_ALLOWED: 80 | //handle 405 method not allowed 81 | break; 82 | case \Scrawler\Router\Router::FOUND: 83 | //call the handler 84 | $response = call_user_func($handler,...$args); 85 | // Send Response 86 | //echo $response 87 | } 88 | 89 | ``` 90 | 91 | Done now whatever request occurs it will be automatically routed . You don't have define a single route 92 |

93 | 94 | ## ✏️ Manual routing 95 | Information on manual routing can be found in [docs](https://component.scrawlerlabs.com/router/) 96 |

97 | 98 | 99 | ## 🦊 How it Works? 100 | 101 | The automatic routing is possible by following some conventions. Lets take a example lets say a controller Hello 102 | 103 | ```php 104 |
117 | 118 | ## 🔥 How does it do it automatically? 119 | 120 | Each request to the server is interpreted by Scrawler Router in following way: 121 | 122 | `METHOD /controller/function/arguments1/arguments2` 123 | 124 | The controller and function that would be invoked will be 125 | 126 | ```php 127 |
156 | 157 | ## ⁉️ How should I name my function for automatic routing? 158 | 159 | The function name in the controller should be named according to following convention: 160 | `methodFunctionname` 161 | Note:The method should always be written in small and the first word of function name should always start with capital. 162 | Method is the method used while calling url. Valid methods are: 163 | 164 | ``` 165 | all - maps any kind of request method i.e it can be get,post etc 166 | get - mpas url called by GET method 167 | post - maps url called by POST method 168 | put - maps url called by PUT method 169 | delete - maps url called by DELETE method 170 | ``` 171 | Some eg. of valid function names are: 172 | ``` 173 | getArticles, postUser, putResource 174 | ``` 175 | Invalid function names are: 176 | ``` 177 | GETarticles, Postuser, PutResource 178 | ``` 179 |
180 | 181 | ## 🏠 Website home page 182 | Scrawler Router uses a special function name `allIndex()` and special controller name `Main`. So If you want to make a controller for your landing page `\` the controller will be defines as follows 183 | ```php 184 | // Inside main.php 185 | class Main 186 | { 187 | // All request to your landing page will be resolved to this controller 188 | // ALternatively you can use getIndex() to resolve only get request 189 | public function allIndex() 190 | { 191 | } 192 | } 193 | ``` 194 |
195 | 196 | ## 🌟 Main Controller 197 | Class name with `Main` signifies special meaning in Scrawler Router , if you wanna define pages route URL you can use main controler 198 | ```php 199 | // Inside main.php 200 | class Main 201 | { 202 | // Resolves `/` 203 | public function getIndex() 204 | { 205 | } 206 | 207 | // Resolves `/abc` 208 | public function getAbc() 209 | { 210 | 211 | } 212 | 213 | // Resolves `/hello` 214 | public function getHello() 215 | { 216 | 217 | } 218 | } 219 | ``` 220 |
221 | 222 | ## 👉 Index function 223 | Just like `Main` controller `allIndex(), getIndex(), postIndex()` etc signifies a special meaning , urls with only controller name and no function name will try to resolve into this function. 224 | ```php 225 | // Inside hello.php 226 | class Hello 227 | { 228 | // Resolves `/hello` 229 | public function getIndex() 230 | { 231 | 232 | } 233 | 234 | // Resolves `/hello/abc` 235 | public function getAbc() 236 | { 237 | 238 | } 239 | } 240 | ``` 241 |
242 | 243 | 244 | ## 👏 Supporters 245 | If you have reached here consider giving a star to help this project ❤️ 246 | [![Stargazers repo roster for @scrawler-labs/router](https://reporoster.com/stars/dark/notext/scrawler-labs/router)](https://github.com/scrawler-labs/router/stargazers) 247 | 248 | Thank You for your forks and contributions 249 | [![Forkers repo roster for @scrawler-labs/router](https://reporoster.com/forks/dark/notext/scrawler-labs/router)](https://github.com/scrawler-labs/router/network/members) 250 |

251 | 252 | ## 🖥️ Server Configuration 253 | 254 | #### Apache 255 | 256 | You may need to add the following snippet in your Apache HTTP server virtual host configuration or **.htaccess** file. 257 | 258 | ```apacheconf 259 | RewriteEngine on 260 | RewriteCond %{REQUEST_FILENAME} !-f 261 | RewriteCond %{REQUEST_FILENAME} !-d 262 | RewriteCond $1 !^(index\.php) 263 | RewriteRule ^(.*)$ /index.php/$1 [L] 264 | ``` 265 | 266 | Alternatively, if you’re lucky enough to be using a version of Apache greater than 2.2.15, then you can instead just use this one, single line: 267 | ```apacheconf 268 | FallbackResource /index.php 269 | ``` 270 | 271 | #### IIS 272 | 273 | For IIS you will need to install URL Rewrite for IIS and then add the following rule to your `web.config`: 274 | ```xml 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | ``` 292 | 293 | #### Nginx 294 | 295 | Under the `server` block of your virtual host configuration, you only need to add three lines. 296 | ```conf 297 | location / { 298 | try_files $uri $uri/ /index.php?$args; 299 | } 300 | ``` 301 | 302 | ## 📄 License 303 | 304 | Scrawler Router is created by [Pranjal Pandey](https://www.physcocode.com) and released under 305 | the MIT License. 306 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrawler/router", 3 | "type": "library", 4 | "description": "An Fully Automatic RESTful PHP Router.", 5 | "keywords": [ 6 | "framework", 7 | "php", 8 | "router", 9 | "restful", 10 | "automatic" 11 | ], 12 | "homepage": "http://github.com/scrawler-php/router", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Pranjal Pandey", 17 | "email": "itspranjalpandey@gmail.com", 18 | "homepage": "https://ipranjal.com/", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "psr/simple-cache": "^3.0", 24 | "thecodingmachine/safe": "^3.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Scrawler\\Router\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Tests\\": "tests/" 34 | } 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "^11.0", 38 | "pestphp/pest": "^3.0", 39 | "phpstan/phpstan": "^2.0", 40 | "symfony/cache": "^7.1", 41 | "thecodingmachine/phpstan-safe-rule": "^1.2", 42 | "rector/rector": "^2.0" 43 | }, 44 | "config": { 45 | "allow-plugins": { 46 | "pestphp/pest-plugin": true, 47 | "dealerdirect/phpcodesniffer-composer-installer": true 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/src', 10 | __DIR__ . '/tests', 11 | ]) 12 | // uncomment to reach your current PHP version 13 | ->withPhpSets(php83:true) 14 | ->withAttributesSets() 15 | ->withPreparedSets(deadCode:true,typeDeclarations:true,codeQuality:true); 16 | -------------------------------------------------------------------------------- /src/.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request,workflow_dispatch] 3 | jobs: 4 | arca-tests: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | operating-system: ['ubuntu-latest'] 9 | php-versions: ['8. 1'] 10 | phpunit-versions: ['latest'] 11 | include: 12 | - operating-system: 'ubuntu-latest' 13 | php-versions: '8.1' 14 | services: 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: nanasess/setup-php@master 19 | with: 20 | php-version: '8.1' 21 | - name: Update Composer 22 | run: sudo composer self-update --no-interaction 23 | - name: Run Composer Install 24 | run: composer install --no-interaction 25 | - name: run tests 26 | run: vendor/bin/pest 27 | -------------------------------------------------------------------------------- /src/RouteCollection.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Scrawler\Router; 15 | 16 | /** 17 | * Collection of all available controllers. 18 | */ 19 | final class RouteCollection 20 | { 21 | /** 22 | * Stores all controller and corrosponding route. 23 | * 24 | * @var array 25 | */ 26 | private array $controllers = []; 27 | 28 | /** 29 | * Stores all manual route. 30 | * 31 | * @var array 32 | */ 33 | private array $route = []; 34 | 35 | /* 36 | * Stores the path of dirctory containing controllers 37 | * @var string 38 | */ 39 | private string $directory; 40 | 41 | /* 42 | * Stores the namespace of controllers 43 | * @var string 44 | */ 45 | private string $namespace; 46 | 47 | /** 48 | * Stores list of directories. 49 | * 50 | * @var array 51 | */ 52 | private array $dir = []; 53 | 54 | /** 55 | * Stores caching engine. 56 | */ 57 | private \Psr\SimpleCache\CacheInterface $cache; 58 | 59 | /** 60 | * Check if caches is enable. 61 | */ 62 | private bool $enableCache = false; 63 | 64 | /** 65 | * Check if auto register is enable. 66 | */ 67 | private bool $autoRegistered = false; 68 | 69 | public function register(string $directory, string $namespace): void 70 | { 71 | $this->autoRegistered = true; 72 | $this->directory = $directory; 73 | $this->namespace = $namespace; 74 | 75 | $this->autoRegister(); 76 | } 77 | 78 | /** 79 | * Function returns the class of corrosponding controller. 80 | */ 81 | public function getController(string $controller): false|string 82 | { 83 | if ($this->enableCache && $this->cache->has(str_replace('/', '_', $controller))) { 84 | return $this->cache->get(str_replace('/', '_', $controller)); 85 | } 86 | 87 | foreach ($this->controllers as $key => $value) { 88 | if ($key === $controller) { 89 | return $value; 90 | } 91 | } 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * Returns cache engine. 98 | */ 99 | public function getCache(): \Psr\SimpleCache\CacheInterface 100 | { 101 | return $this->cache; 102 | } 103 | 104 | /** 105 | * Register controller with route collection. 106 | */ 107 | public function registerController(string $name, string $class): void 108 | { 109 | $this->controllers[$name] = $class; 110 | if ($this->enableCache) { 111 | $this->cache->set(str_replace('/', '_', $name), $class); 112 | $this->cache->set('collection', $this->controllers); 113 | } 114 | } 115 | 116 | /** 117 | * Automatically register all controllers in specified directory. 118 | */ 119 | private function autoRegister(): void 120 | { 121 | $files = array_slice(\Safe\scandir($this->directory), 2); 122 | foreach ($files as $file) { 123 | if (is_dir($this->directory.'/'.$file)) { 124 | $this->registerDir($file); 125 | $dir = $this->directory.'/'.$file; 126 | $dir_files = array_slice(\Safe\scandir($dir), 2); 127 | foreach ($dir_files as $dir_file) { 128 | if ('Main.php' !== $dir_file && !\is_dir($dir.'/'.$dir_file)) { 129 | $this->registerController($file.'/'.\basename((string) $dir_file, '.php'), $this->namespace.'\\'.\ucfirst((string) $file).'\\'.\basename((string) $dir_file, '.php')); 130 | } 131 | } 132 | } 133 | if ('Main.php' !== $file && !\is_dir($this->directory.'/'.$file)) { 134 | $this->registerController(\basename((string) $file, '.php'), $this->namespace.'\\'.\basename((string) $file, '.php')); 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Function to get the namespace of controllers. 141 | */ 142 | public function getNamespace(): string 143 | { 144 | return $this->namespace; 145 | } 146 | 147 | /** 148 | * Function to return list of all controller currently registerd with route collction. 149 | * 150 | * @return array 151 | */ 152 | public function getControllers(): array 153 | { 154 | return $this->controllers; 155 | } 156 | 157 | /** 158 | * Function to add dir to list of dir. 159 | */ 160 | public function registerDir(string $dir): void 161 | { 162 | $this->dir[] = $dir; 163 | 164 | if ($this->enableCache) { 165 | $this->cache->set('dir', $this->dir); 166 | } 167 | } 168 | 169 | /** 170 | * Function to check if cache is enabled. 171 | */ 172 | public function isCacheEnabled(): bool 173 | { 174 | return $this->enableCache; 175 | } 176 | 177 | /** 178 | * Function to check if its a registered dir. 179 | */ 180 | public function isDir(string $dir): bool 181 | { 182 | if ($this->enableCache && $this->cache->has('dir')) { 183 | $this->dir = $this->cache->get('dir'); 184 | } 185 | 186 | return in_array($dir, $this->dir); 187 | } 188 | 189 | /** 190 | * Enable cache with custom cache engine. 191 | */ 192 | public function enableCache(\Psr\SimpleCache\CacheInterface $cache): void 193 | { 194 | $this->cache = $cache; 195 | $this->enableCache = true; 196 | if ($this->cache->has('collection')) { 197 | $this->controllers = $this->cache->get('collection'); 198 | } 199 | } 200 | 201 | /** 202 | * Register manual route in route collection. 203 | */ 204 | private function registerManual(string $method, string $route, callable $callable): void 205 | { 206 | $this->route[$method][$route] = $callable; 207 | } 208 | 209 | /** 210 | * register manual get route. 211 | */ 212 | public function get(string $route, callable $callable): void 213 | { 214 | $this->registerManual('get', $route, $callable); 215 | } 216 | 217 | /** 218 | * register manual post route. 219 | */ 220 | public function post(string $route, callable $callable): void 221 | { 222 | $this->registerManual('post', $route, $callable); 223 | } 224 | 225 | /** 226 | * register manual put route. 227 | */ 228 | public function put(string $route, callable $callable): void 229 | { 230 | $this->registerManual('put', $route, $callable); 231 | } 232 | 233 | /** 234 | * register manual delete route. 235 | */ 236 | public function delete(string $route, callable $callable): void 237 | { 238 | $this->registerManual('delete', $route, $callable); 239 | } 240 | 241 | /** 242 | * register manual patch route. 243 | */ 244 | public function all(string $route, callable $callable): void 245 | { 246 | $this->registerManual('all', $route, $callable); 247 | } 248 | 249 | /** 250 | * get callable using route and method. 251 | */ 252 | public function getRoute(string $route, string $method): callable|bool 253 | { 254 | return $this->route[$method][$route] ?? $this->route['all'][$route] ?? false; 255 | } 256 | 257 | /** 258 | * get all routes. 259 | * 260 | * @return array 261 | */ 262 | public function getRoutes(): array 263 | { 264 | return $this->route; 265 | } 266 | 267 | /** 268 | * check if auto register is enable. 269 | */ 270 | public function isAutoRegistered(): bool 271 | { 272 | return $this->autoRegistered; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/Router.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Scrawler\Router; 15 | 16 | /** 17 | * This class is used when it is used as stand alone router. 18 | */ 19 | final readonly class Router 20 | { 21 | // ---------------------------------------------------------------// 22 | 23 | /** 24 | * Stores the RouterCollection object. 25 | */ 26 | private RouteCollection $collection; 27 | 28 | /** 29 | * Stores the Engine Instance. 30 | */ 31 | private RouterEngine $engine; 32 | 33 | public const NOT_FOUND = 0; 34 | public const FOUND = 1; 35 | public const METHOD_NOT_ALLOWED = 2; 36 | 37 | // ---------------------------------------------------------------// 38 | 39 | /** 40 | * constructor overloading for auto routing. 41 | */ 42 | public function __construct() 43 | { 44 | $this->collection = new RouteCollection(); 45 | $this->engine = new RouterEngine($this->collection); 46 | } 47 | 48 | // ---------------------------------------------------------------// 49 | 50 | /** 51 | * constructor overloading for auto routing. 52 | */ 53 | public function register(string $dir, string $namespace): void 54 | { 55 | $this->collection->register($dir, $namespace); 56 | } 57 | 58 | // ---------------------------------------------------------------// 59 | 60 | /** 61 | * Enable cache. 62 | */ 63 | public function enableCache(\Psr\SimpleCache\CacheInterface $cache): void 64 | { 65 | $this->collection->enableCache($cache); 66 | } 67 | 68 | // ---------------------------------------------------------------// 69 | /** 70 | * Dispatch function. 71 | * 72 | * @return array 73 | */ 74 | public function dispatch(string $httpMethod, string $uri): array 75 | { 76 | $result = $this->engine->route($httpMethod, $uri); 77 | 78 | if (0 === $result[0] || 2 === $result[0]) { 79 | return $result; 80 | } 81 | 82 | if (\is_callable($result[1])) { 83 | return $result; 84 | } 85 | 86 | [$class, $method] = explode('::', (string) $result[1], 2); 87 | $result[1] = [new $class(), $method]; 88 | 89 | return $result; 90 | } 91 | 92 | /** 93 | * register manual get route. 94 | */ 95 | public function get(string $route, callable $callable): void 96 | { 97 | $this->collection->get($route, $callable); 98 | } 99 | 100 | /** 101 | * Register manual post route. 102 | */ 103 | public function post(string $route, callable $callable): void 104 | { 105 | $this->collection->post($route, $callable); 106 | } 107 | 108 | /** 109 | * Register manual put route. 110 | */ 111 | public function put(string $route, callable $callable): void 112 | { 113 | $this->collection->put($route, $callable); 114 | } 115 | 116 | /** 117 | * Register manual delete route. 118 | */ 119 | public function delete(string $route, callable $callable): void 120 | { 121 | $this->collection->delete($route, $callable); 122 | } 123 | 124 | /** 125 | * Register manual all route. 126 | */ 127 | public function all(string $route, callable $callable): void 128 | { 129 | $this->collection->all($route, $callable); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/RouterEngine.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Scrawler\Router; 15 | 16 | /** 17 | * This class routes the URL to corrosponding controller. 18 | */ 19 | final class RouterEngine 20 | { 21 | // ---------------------------------------------------------------// 22 | 23 | /** 24 | * Stores the URL broken logic wise. 25 | * 26 | * @var array 27 | */ 28 | private array $pathInfo = []; 29 | 30 | /** 31 | * Stores the request method i.e get,post etc. 32 | */ 33 | private string $httpMethod; 34 | 35 | /** 36 | * Stores the request uri. 37 | */ 38 | private string $uri; 39 | 40 | /** 41 | * Stores dir mode. 42 | */ 43 | private bool $dirMode = false; 44 | 45 | /** 46 | * Store Dirctory during dir Mode. 47 | */ 48 | private string $dir = ''; 49 | 50 | /** 51 | * stores debug msg. 52 | */ 53 | private string $debugMsg = ''; 54 | 55 | // ---------------------------------------------------------------// 56 | 57 | /** 58 | * constructor overloading for auto routing. 59 | */ 60 | public function __construct( 61 | /** 62 | * Stores the RouterCollection object. 63 | */ 64 | private readonly RouteCollection $collection, 65 | ) { 66 | } 67 | 68 | // ---------------------------------------------------------------// 69 | 70 | /** 71 | * Detects the URL and call the corrosponding method 72 | * of corrosponding controller. 73 | * 74 | * @return array 75 | */ 76 | public function route(string $httpMethod, string $uri): array 77 | { 78 | $this->httpMethod = strtolower($httpMethod); 79 | $this->uri = $uri; 80 | 81 | // Break URL into segments 82 | $this->pathInfo = explode('/', $uri); 83 | array_shift($this->pathInfo); 84 | 85 | // Try manual routing 86 | [$status, $handler, $args] = $this->routeManual(); 87 | if ($status) { 88 | return [1, $handler, $args, '']; 89 | } 90 | 91 | // Try auto routing 92 | if ($this->collection->isAutoRegistered()) { 93 | return $this->routeAuto(); 94 | } 95 | 96 | return [0, '', [], $this->debugMsg]; 97 | } 98 | 99 | /** 100 | * Set Arguments on the request object. 101 | * 102 | * @return array 103 | */ 104 | private function routeAuto(): array 105 | { 106 | $controller = $this->getController(); 107 | $method = $this->getMethod($controller); 108 | if ('' === $method) { 109 | if ($this->checkMethodNotAllowed($controller)) { 110 | return [2, '', [], $this->debugMsg]; 111 | } 112 | 113 | return [0, '', [], $this->debugMsg]; 114 | } 115 | $handler = $controller.'::'.$method; 116 | $arguments = $this->getArguments($controller, $method); 117 | 118 | if (is_bool($arguments) && !$arguments) { 119 | return [0, '', [], $this->debugMsg]; 120 | } 121 | 122 | return [1, $handler, $arguments, '']; 123 | } 124 | 125 | /** 126 | * Function to get namespace. 127 | */ 128 | private function getNamespace(): string 129 | { 130 | if ($this->dirMode) { 131 | return $this->collection->getNamespace().'\\'.$this->dir; 132 | } 133 | 134 | return $this->collection->getNamespace(); 135 | } 136 | 137 | // ---------------------------------------------------------------// 138 | 139 | /** 140 | * Function to get controller. 141 | */ 142 | private function getController(): string 143 | { 144 | $controller = ucfirst($this->pathInfo[0]); 145 | 146 | if (isset($this->pathInfo[0]) && $this->collection->isDir(ucfirst($this->pathInfo[0]))) { 147 | $this->dir = ucfirst($this->pathInfo[0]); 148 | $this->dirMode = true; 149 | array_shift($this->pathInfo); 150 | } 151 | 152 | if ($this->dirMode && isset($this->pathInfo[0])) { 153 | $controller = $this->dir.'/'.ucfirst($this->pathInfo[0]); 154 | } 155 | 156 | // Set corrosponding controller 157 | if (isset($this->pathInfo[0]) && '' !== $this->pathInfo[0] && '0' !== $this->pathInfo[0]) { 158 | $controller = $this->collection->getController($controller); 159 | } else { 160 | $controller = $this->getNamespace().'\Main'; 161 | } 162 | 163 | if (!$controller) { 164 | $controller = ''; 165 | } 166 | 167 | if (class_exists($controller)) { 168 | return $controller; 169 | } 170 | 171 | $controller = $this->getNamespace().'\Main'; 172 | 173 | if (class_exists($controller)) { 174 | array_unshift($this->pathInfo, ''); 175 | 176 | return $controller; 177 | } 178 | 179 | $this->debug('No Controller could be resolved:'.$controller); 180 | 181 | return ''; 182 | } 183 | 184 | /** 185 | * Function to throw 404 error. 186 | */ 187 | private function debug(string $message): void 188 | { 189 | $this->debugMsg = $message; 190 | } 191 | 192 | // ---------------------------------------------------------------// 193 | 194 | /** 195 | * Function to dispach the method if method exist. 196 | * 197 | * @return bool|array 198 | */ 199 | private function getArguments(string $controller, string $method): bool|array 200 | { 201 | $controllerObj = new $controller(); 202 | 203 | $arguments = []; 204 | $counter = count($this->pathInfo); 205 | for ($j = 2; $j < $counter; ++$j) { 206 | $arguments[] = $this->pathInfo[$j]; 207 | } 208 | // Check weather arguments are passed else throw a 404 error 209 | $classMethod = new \ReflectionMethod($controllerObj, $method); 210 | $params = $classMethod->getParameters(); 211 | // Remove params if it allows null 212 | foreach ($params as $key => $param) { 213 | if ($param->isOptional()) { 214 | unset($params[$key]); 215 | } 216 | } 217 | if (count($arguments) < count($params)) { 218 | $this->debug('Not enough arguments given to the method'); 219 | 220 | return false; 221 | } 222 | // finally fix the long awaited allIndex bug ! 223 | if (count($arguments) > count($classMethod->getParameters())) { 224 | $this->debug('Not able to resolve '.$method.'for'.$controller.'controller'); 225 | 226 | return false; 227 | } 228 | 229 | return $arguments; 230 | } 231 | 232 | /** 233 | * Function to check for 405. 234 | */ 235 | private function checkMethodNotAllowed(string $controller): bool 236 | { 237 | if (!isset($this->pathInfo[1])) { 238 | return false; 239 | } 240 | 241 | return method_exists($controller, 'get'.ucfirst($this->pathInfo[1])) || method_exists($controller, 'post'.ucfirst($this->pathInfo[1])) || method_exists($controller, 'put'.ucfirst($this->pathInfo[1])) || method_exists($controller, 'delete'.ucfirst($this->pathInfo[1])); 242 | } 243 | 244 | /** 245 | * Returns the method to be called according to URL. 246 | */ 247 | private function getMethod(string $controller): string 248 | { 249 | // Set Method from second argument from URL 250 | if (isset($this->pathInfo[1])) { 251 | if (method_exists($controller, $function = $this->httpMethod.ucfirst($this->pathInfo[1]))) { 252 | return $function; 253 | } 254 | if (method_exists($controller, $function = 'all'.ucfirst($this->pathInfo[1]))) { 255 | return $function; 256 | } 257 | } 258 | 259 | if (isset($function)) { 260 | $last_function = $function; 261 | } 262 | if (method_exists($controller, $function = $this->httpMethod.'Index')) { 263 | array_unshift($this->pathInfo, ''); 264 | 265 | return $function; 266 | } 267 | // Last attempt to invoke allIndex 268 | if (method_exists($controller, $function = 'allIndex')) { 269 | array_unshift($this->pathInfo, ''); 270 | 271 | return $function; 272 | } 273 | 274 | if (isset($last_function)) { 275 | $this->debug('Neither '.$function.' method nor '.$last_function.' method you found in '.$controller.' controller'); 276 | 277 | return ''; 278 | } 279 | 280 | $this->debug($function.' method not found in '.$controller.' controller'); 281 | 282 | return ''; 283 | } 284 | 285 | /** 286 | * Function to route manual routes. 287 | * 288 | * @return array 289 | */ 290 | private function routeManual(): array 291 | { 292 | $controller = null; 293 | $arguments = []; 294 | $routes = $this->collection->getRoutes(); 295 | $collection_route = $this->collection->getRoute($this->uri, $this->httpMethod); 296 | if ($collection_route) { 297 | $controller = $collection_route; 298 | } elseif ([] !== $routes) { 299 | $tokens = [ 300 | ':string' => '([a-zA-Z]+)', 301 | ':number' => '(\d+)', 302 | ':alpha' => '([a-zA-Z0-9-_]+)', 303 | ]; 304 | 305 | foreach ($routes[$this->httpMethod] as $pattern => $handler_name) { 306 | $pattern = strtr($pattern, $tokens); 307 | if (0 !== \Safe\preg_match('#^/?'.$pattern.'/?$#', $this->uri, $matches)) { 308 | $controller = $handler_name; 309 | $arguments = $matches; 310 | break; 311 | } 312 | } 313 | } 314 | 315 | if (is_callable($controller)) { 316 | unset($arguments[0]); 317 | 318 | return [true, $controller, $arguments]; 319 | } 320 | 321 | return [false, '', '']; 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /tests/Demo/App/Main.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 17 | 18 | 19 | function getCollection($cache): \Scrawler\Router\RouteCollection{ 20 | $collection = new \Scrawler\Router\RouteCollection(); 21 | if($cache){ 22 | $cache = new FilesystemAdapter(); 23 | $cache = new Psr16Cache($cache); 24 | 25 | $collection->enableCache($cache); 26 | } 27 | $collection->register(__DIR__."/Demo","Tests\Demo"); 28 | 29 | return $collection; 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/ManualRouteTest.php: -------------------------------------------------------------------------------- 1 | preset()->php(); 3 | arch()->preset()->security(); 4 | arch()->preset()->strict(); 5 | 6 | it('tests manual route ', function (bool $cache): void { 7 | 8 | 9 | $this->router = new \Scrawler\Router\Router(); 10 | $this->router->get('/testo',fn(): string => 'Hello'); 11 | 12 | $response = $this->router->dispatch('get','/testo'); 13 | $response = call_user_func($response[1]); 14 | expect($response)->toBe('Hello'); 15 | 16 | $this->router->post('/testo',fn(): string => 'Hello post'); 17 | 18 | $response = $this->router->dispatch('post','/testo'); 19 | $response = call_user_func($response[1]); 20 | expect($response)->toBe('Hello post'); 21 | 22 | $this->router->delete('/testo',fn(): string => 'Hello delete'); 23 | 24 | 25 | $response = $this->router->dispatch('delete','/testo'); 26 | $response = call_user_func($response[1]); 27 | 28 | expect($response)->toBe('Hello delete'); 29 | 30 | $this->router->put('/testo',fn(): string => 'Hello put'); 31 | 32 | 33 | $response = $this->router->dispatch('put','/testo'); 34 | $response = call_user_func($response[1]); 35 | 36 | expect($response)->toBe('Hello put'); 37 | 38 | $this->router->all('/testall',fn(): string => 'Hello all'); 39 | 40 | $response = $this->router->dispatch('post','/testall'); 41 | $response = call_user_func($response[1]); 42 | expect($response)->toBe('Hello all'); 43 | 44 | $response = $this->router->dispatch('get','/testall'); 45 | $response = call_user_func($response[1]); 46 | expect($response)->toBe('Hello all'); 47 | 48 | 49 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); 50 | 51 | it('tests manual route with parameters', function (bool $cache): void { 52 | 53 | $this->router = new \Scrawler\Router\Router(); 54 | 55 | $this->router->get('/testo/:alpha',fn($name): string => 'Hello '.$name); 56 | 57 | [$status,$handler,$args,$debug] = $this->router->dispatch('get','/testo/sam'); 58 | $response = call_user_func($handler,...$args); 59 | 60 | expect($response)->toBe('Hello sam'); 61 | 62 | $this->router->get('/test/num/:number',fn($num): string => 'num '.$num); 63 | 64 | [$status,$handler,$args,$debug] = $this->router->dispatch('get', '/test/num/5'); 65 | $response = call_user_func($handler,...$args); 66 | 67 | expect($response)->toBe('num 5'); 68 | 69 | 70 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); 71 | 72 | it('tests manual route with parameter advance', function (bool $cache): void { 73 | 74 | $this->router = new \Scrawler\Router\Router(); 75 | 76 | $this->router->get('/testo/:alpha/test',fn($name): string => 'Hello '.$name); 77 | 78 | [$status,$handler,$args,$debug] = $this->router->dispatch('get','/testo/sam/test'); 79 | $response = call_user_func($handler,...$args); 80 | 81 | expect($response)->toBe('Hello sam'); 82 | 83 | $this->router->get('/test/:number/num',fn($num): string => 'num '.$num); 84 | 85 | [$status,$handler,$args,$debug] = $this->router->dispatch('get', '/test/5/num'); 86 | $response = call_user_func($handler,...$args); 87 | 88 | expect($response)->toBe('num 5'); 89 | 90 | 91 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); -------------------------------------------------------------------------------- /tests/Unit/RouterCollectionTest.php: -------------------------------------------------------------------------------- 1 | getController('Hello'))->toBe(\Tests\Demo\Hello::class); 6 | expect($collection->getController('Bye'))->toBe(\Tests\Demo\Bye::class); 7 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); 8 | 9 | it('tests registerController() method',function(bool $cache): void{ 10 | $collection = getCollection($cache); 11 | $collection->registerController('Test',\Tests\Demo\Test::class); 12 | expect($collection->getController('Test'))->toBe(\Tests\Demo\Test::class); 13 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); 14 | 15 | it('tests getNamepace() method',function(bool $cache): void{ 16 | $collection = getCollection($cache); 17 | expect($collection->getNamespace())->toBe('Tests\Demo'); 18 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); 19 | 20 | it('tests getControllers() method',function(bool $cache): void{ 21 | $collection = getCollection($cache); 22 | expect($collection->getControllers())->toHaveKey('Hello'); 23 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); 24 | 25 | it('tests cache related methods',function(): void{ 26 | $collection = getCollection(true); 27 | expect($collection->getCache())->toBeInstanceOf(\Psr\SimpleCache\CacheInterface ::class); 28 | expect($collection->isCacheEnabled())->toBe(true); 29 | }); 30 | 31 | it('tests for dir',function(): void{ 32 | $collection = getCollection(true); 33 | //expect($collection->getCache())->toBeInstanceOf(Kodus\Cache\FileCache::class); 34 | expect($collection->isDir('App'))->toBe(true); 35 | }); -------------------------------------------------------------------------------- /tests/Unit/RouterEngineTest.php: -------------------------------------------------------------------------------- 1 | preset()->php(); 3 | arch()->preset()->security(); 4 | arch()->preset()->strict(); 5 | 6 | it('tests normal controller resolving',function(bool $cache): void{ 7 | 8 | $collection = getCollection($cache); 9 | 10 | 11 | $engine = new \Scrawler\Router\RouterEngine($collection); 12 | [$status,$handler,$args,$debug] = $engine->route('GET','/hello/world/pranjal'); 13 | 14 | expect($handler)->toBe('Tests\Demo\Hello::getWorld'); 15 | expect($args)->toBe(['pranjal']); 16 | 17 | 18 | [$status,$handler,$args,$debug] = $engine->route('GET','/bye'); 19 | 20 | expect($handler)->toBe('Tests\Demo\Bye::allIndex'); 21 | 22 | 23 | [$status,$handler,$args,$debug] = $engine->route('GET','/bye/test'); 24 | 25 | expect($handler)->toBe('Tests\Demo\Bye::allTest'); 26 | 27 | 28 | })->with(['cacheDisabled'=>false,'cacheEnabled'=>true]); 29 | 30 | it('tests controller resolver inside directory',function(bool $cache): void{ 31 | $collection = getCollection($cache); 32 | 33 | 34 | $engine = new \Scrawler\Router\RouterEngine($collection); 35 | [$status,$handler,$args,$debug] = $engine->route('GET','/app/test'); 36 | 37 | expect($handler)->toBe('Tests\Demo\App\Test::getIndex'); 38 | 39 | 40 | [$status,$handler,$args,$debug] = $engine->route('GET','/app/test/hi'); 41 | 42 | expect($handler)->toBe('Tests\Demo\App\Test::getHi'); 43 | 44 | 45 | [$status,$handler,$args,$debug] = $engine->route('GET','/app'); 46 | expect($handler)->toBe('Tests\Demo\App\Main::getIndex'); 47 | 48 | })->with(['cacheDisabled'=>false,'cacheEnabled'=>true]); 49 | 50 | it('tests main controller resolving',function(bool $cache): void{ 51 | $collection = getCollection($cache); 52 | 53 | 54 | $engine = new \Scrawler\Router\RouterEngine($collection); 55 | [$status,$handler,$args,$debug] = $engine->route('GET','/pranjal'); 56 | 57 | expect($handler)->toBe('Tests\Demo\Main::getIndex'); 58 | expect($args)->toBe(['pranjal']); 59 | 60 | 61 | [$status,$handler,$args,$debug] = $engine->route('GET','/hi'); 62 | expect($handler)->toBe('Tests\Demo\Main::getHi'); 63 | 64 | })->with(['cacheDisabled'=>false,'cacheEnabled'=>true]); 65 | 66 | it('tests controller not found ',function(): void{ 67 | 68 | $engine = new \Scrawler\Router\RouterEngine(getCollection(false)); 69 | [$status,$handler,$args,$debug] = $engine->route('GET','/random/r'); 70 | expect($status)->toBe(0); 71 | 72 | [$status,$handler,$args,$debug] = $engine->route('GET','/appo/random'); 73 | expect($status)->toBe(0); 74 | 75 | }); 76 | 77 | it('tests for method not allowed ',function(): void{ 78 | 79 | $engine = new \Scrawler\Router\RouterEngine(getCollection(false)); 80 | [$status,$handler,$args,$debug] = $engine->route('GET','/random/r'); 81 | expect($status)->toBe(0); 82 | 83 | [$status,$handler,$args,$debug] = $engine->route('GET','/appo/random'); 84 | expect($status)->toBe(0); 85 | 86 | }); 87 | 88 | it('tests method not found exception',function(): void{ 89 | 90 | $engine = new \Scrawler\Router\RouterEngine(getCollection(false)); 91 | [$status,$handler,$args,$debug] = $engine->route('GET','/test/worl'); 92 | expect($status)->toBe(0); 93 | 94 | 95 | }); 96 | 97 | it('tests method not allowed exception',function(): void{ 98 | 99 | $engine = new \Scrawler\Router\RouterEngine(getCollection(false)); 100 | [$status,$handler,$args,$debug] = $engine->route('POST','/hello/world/pranjal'); 101 | 102 | expect($status)->toBe(\Scrawler\Router\Router::METHOD_NOT_ALLOWED); 103 | 104 | }); 105 | 106 | it('tests no route found exception',function(): void{ 107 | 108 | $engine = new \Scrawler\Router\RouterEngine(new \Scrawler\Router\RouteCollection()); 109 | [$status,$handler,$args,$debug] = $engine->route('GET','/random/route'); 110 | 111 | expect($status)->toBe(\Scrawler\Router\Router::NOT_FOUND); 112 | 113 | }); 114 | 115 | 116 | it('tests method call with optional parameter',function(): void{ 117 | 118 | $engine = new \Scrawler\Router\RouterEngine(getCollection(false)); 119 | [$status,$handler,$args,$debug] = $engine->route('GET',uri: '/param'); 120 | expect($handler)->toBe('Tests\Demo\Param::allIndex'); 121 | [$status,$handler,$args,$debug] = $engine->route('GET',uri: '/param/12'); 122 | expect($handler)->toBe('Tests\Demo\Param::allIndex'); 123 | [$status,$handler,$args,$debug] = $engine->route('GET',uri: '/param/test'); 124 | expect($handler)->toBe('Tests\Demo\Param::getTest'); 125 | [$status,$handler,$args,$debug] = $engine->route('GET',uri: '/param/test/12'); 126 | expect($handler)->toBe('Tests\Demo\Param::getTest'); 127 | 128 | }); -------------------------------------------------------------------------------- /tests/Unit/RouterTest.php: -------------------------------------------------------------------------------- 1 | preset()->php(); 4 | arch()->preset()->security(); 5 | arch()->preset()->strict(); 6 | 7 | 8 | it('tests router dispatch method ', function (bool $cache): void { 9 | 10 | 11 | $this->router = new \Scrawler\Router\Router(); 12 | $this->router->register(__DIR__."/../Demo","Tests\Demo"); 13 | [$status,$handler,$args,$debug] = $this->router->dispatch('GET','/hello/world/pranjal'); 14 | 15 | $response = call_user_func($handler,...$args); 16 | 17 | expect($status)->toBe(\Scrawler\Router\Router::FOUND); 18 | expect($response)->toBe('Hello pranjal'); 19 | 20 | $collection = getCollection(true); 21 | $cache = $collection->getCache(); 22 | $this->router->enableCache($cache); 23 | [$status,$handler,$args,$debug] = $this->router->dispatch('GET','/hello/world'); 24 | expect($status)->toBe(\Scrawler\Router\Router::NOT_FOUND); 25 | 26 | 27 | [$status,$handler,$args,$debug] = $this->router->dispatch('GET','/bye/world/nobody'); 28 | $response = call_user_func($handler,...$args); 29 | 30 | expect($status)->toBe(\Scrawler\Router\Router::FOUND); 31 | expect($response)->toBe('Bye nobody'); 32 | 33 | })->with(['cacheEnabled'=>true,'cacheDisabled'=>false]); 34 | 35 | 36 | 37 | --------------------------------------------------------------------------------