├── .gitattributes ├── .gitignore ├── src ├── Exception.php └── Pop.php ├── composer.json ├── .github └── workflows │ └── phpunit.yml ├── LICENSE.TXT └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/ export-ignore 2 | phpunit.xml export-ignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | public 3 | script 4 | vendor 5 | kettle 6 | .phpunit.result.cache 7 | .phpunit.cache 8 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2009-2026 NOLA Interactive, LLC. 8 | * @license https://www.popphp.org/license New BSD License 9 | */ 10 | 11 | /** 12 | * @namespace 13 | */ 14 | namespace Popcorn; 15 | 16 | /** 17 | * This is the Pop Exception class for Popcorn. 18 | * 19 | * @category Popcorn 20 | * @package Popcorn 21 | * @author Nick Sagona, III 22 | * @copyright Copyright (c) 2009-2026 NOLA Interactive, LLC. 23 | * @license https://www.popphp.org/license New BSD License 24 | * @version 4.1.4 25 | */ 26 | class Exception extends \Exception {} 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "popphp/popcorn", 3 | "description": "Popcorn, A REST-Based PHP Micro Framework", 4 | "keywords": [ 5 | "rest framework", 6 | "php micro framework", 7 | "popcorn" 8 | ], 9 | "homepage": "https://github.com/popphp/popcorn", 10 | "license": "BSD-3-Clause", 11 | "authors": [ 12 | { 13 | "name": "Nick Sagona", 14 | "email": "dev@noladev.com", 15 | "homepage": "https://www.popphp.org/" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.3.0", 20 | "popphp/popphp": "^4.4.0", 21 | "popphp/pop-cookie": "^4.0.4", 22 | "popphp/pop-http": "^5.3.8", 23 | "popphp/pop-session": "^4.0.4", 24 | "popphp/pop-view": "^4.0.4" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^12.0.0" 28 | }, 29 | "suggest": { 30 | "popphp/pop-db": "For DB management and transactions", 31 | "popphp/pop-form": "For HTML form generation and validation" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Popcorn\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Popcorn\\Test\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/phpunit" 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "4.1.x-dev" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: phpunit 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | XDEBUG_MODE: debug,coverage 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | php-versions: [ '8.3', '8.4' ] 19 | phpunit-versions: ['12.0.0'] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php-versions }} 28 | tools: phpunit:${{ matrix.phpunit-versions }} 29 | 30 | #- name: Validate composer.json and composer.lock 31 | # run: composer validate 32 | 33 | - name: Cache Composer packages 34 | id: composer-cache 35 | uses: actions/cache@v3 36 | with: 37 | path: vendor 38 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-php- 41 | 42 | - name: Install dependencies 43 | if: steps.composer-cache.outputs.cache-hit != 'true' 44 | run: composer install --prefer-dist --no-progress --no-suggest 45 | 46 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 47 | # Docs: https://getcomposer.org/doc/articles/scripts.md 48 | 49 | - name: Run test suite 50 | run: composer run-script test 51 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | BSD 3 Clause License 2 | 3 | Copyright (c) 2009-2026, NOLA Interactive, LLC. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of NOLA Interactive, LLC, nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY NOLA INTERACTIVE, LLC, ''AS IS'' AND ANY EXPRESS OR 18 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | EVENT SHALL NOLA INTERACTIVE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 25 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Popcorn PHP Micro Framework 2 | =========================== 3 | 4 | 5 | 6 | [![Build Status](https://github.com/popphp/popcorn/workflows/phpunit/badge.svg)](https://github.com/popphp/popcorn/actions) 7 | [![Coverage Status](http://cc.popphp.org/coverage.php?comp=popcorn)](http://cc.popphp.org/popcorn/) 8 | 9 | [![Join the chat at https://discord.gg/TZjgT74U7E](https://media.popphp.org/img/discord.svg)](https://discord.gg/TZjgT74U7E) 10 | 11 | * [Overview](#overview) 12 | * [Install](#install) 13 | * [Quickstart](#quickstart) 14 | * [Advanced](#advanced) 15 | * [Custom Methods](#custom-methods) 16 | 17 | RELEASE INFORMATION 18 | ------------------- 19 | 20 | Popcorn PHP REST-Based Micro Framework 4.1.2 21 | Released December 2, 2024 22 | 23 | Overview 24 | -------- 25 | 26 | Popcorn PHP Micro Framework is a REST-based micro framework. It is a small component 27 | that acts as a layer for [Pop PHP](https://github.com/popphp/popphp) to enforce the REST-based routing rules of a 28 | web application. It supports PHP 8.2+. 29 | 30 | `popcorn` is a component of [Pop PHP Framework](http://www.popphp.org/). 31 | 32 | [Top](#popcorn-php-micro-framework) 33 | 34 | Install 35 | ------- 36 | 37 | Install `popcorn` using Composer. 38 | 39 | composer require popphp/popcorn 40 | 41 | Or, require it in your composer.json file 42 | 43 | "require": { 44 | "popphp/popcorn" : "^4.1.4" 45 | } 46 | 47 | [Top](#popcorn-php-micro-framework) 48 | 49 | Quickstart 50 | ---------- 51 | 52 | In a simple `index.php` file, you can define the routes you want to allow 53 | in your application. In this example, closures are used as the controllers. 54 | The wildcard route `*` can serve as a "catch-all" to handle routes that are 55 | not found or not allowed. 56 | 57 | ```php 58 | use Popcorn\Pop; 59 | 60 | $app = new Pop(); 61 | 62 | // Home page: GET http://localhost/ 63 | $app->get('/', function() { 64 | echo 'Hello World!'; 65 | }); 66 | 67 | // Say hello page: GET http://localhost/hello/world 68 | $app->get('/hello/:name', function($name) { 69 | echo 'Hello ' . ucfirst($name) . '!'; 70 | }); 71 | 72 | // Wildcard route to handle errors 73 | $app->get('*', function() { 74 | header('HTTP/1.1 404 Not Found'); 75 | echo 'Page Not Found.'; 76 | }); 77 | ``` 78 | 79 | The above example defines two `GET` routes and wildcard to handle failures. 80 | 81 | We can define a `POST` route like in this example below: 82 | 83 | ```php 84 | // Post auth route: POST http://localhost/auth 85 | $app->post('/auth', function() { 86 | if ($_SERVER['HTTP_AUTHORIZATION'] == 'my-token') { 87 | echo 'Auth successful'; 88 | } else { 89 | echo 'Auth failed'; 90 | } 91 | }); 92 | 93 | $app->run(); 94 | ``` 95 | 96 | If you attempted access that above URL via GET (or any method that wasn't POST), 97 | it would fail. If you access that URL via POST, but with the wrong token, it will 98 | return the `Auth failed` message as enforced by the application. Access the URL 99 | via POST with the correct token, and it will be successful. 100 | 101 | ```bash 102 | $ curl -X POST --header "Authorization: bad-token" http://localhost/auth 103 | Auth failed 104 | ``` 105 | 106 | ```bash 107 | $ curl -X POST --header "Authorization: my-token" http://localhost/auth 108 | Auth successful 109 | ``` 110 | 111 | [Top](#popcorn-php-micro-framework) 112 | 113 | Advanced 114 | -------- 115 | 116 | In a more advanced example, we can take advantage of more of an MVC-style 117 | of wiring up an application using the core components of Pop PHP with 118 | Popcorn. Keeping it simple, let's look at a controller class 119 | `MyApp\Controller\IndexController` like this: 120 | 121 | ```php 122 | request = $request; 143 | $this->response = $response; 144 | $this->viewPath = __DIR__ . '/../view/'; 145 | } 146 | 147 | public function index(): void 148 | { 149 | $view = new View($this->viewPath . '/index.phtml'); 150 | $view->title = 'Hello'; 151 | 152 | $this->response->setBody($view->render()); 153 | $this->response->send(); 154 | } 155 | 156 | public function hello($name): void 157 | { 158 | $view = new View($this->viewPath . '/index.phtml'); 159 | $view->title = 'Hello ' . $name; 160 | 161 | $this->response->setBody($view->render()); 162 | $this->response->send(); 163 | } 164 | 165 | public function error(): void 166 | { 167 | $view = new View($this->viewPath . '/error.phtml'); 168 | $view->title = 'Error'; 169 | 170 | $this->response->setBody($view->render()); 171 | $this->response->send(404); 172 | } 173 | 174 | } 175 | ``` 176 | 177 | and two view scripts, `index.phtml` and `error.phtml`, respectively: 178 | 179 | ```php 180 | 181 | 182 | 183 | 184 | 185 | <?=$title; ?> 186 | 187 | 188 | 189 |

190 | 191 | 192 | 193 | ``` 194 | 195 | ```php 196 | 197 | 198 | 199 | 200 | 201 | <?=$title; ?> 202 | 203 | 204 | 205 |

206 |

Sorry, that page was not found.

207 | 208 | 209 | 210 | ``` 211 | 212 | Then we can set the app like this: 213 | 214 | ```php 215 | use Popcorn\Pop; 216 | 217 | $app = new Pop(); 218 | 219 | $app->get('/', [ 220 | 'controller' => 'MyApp\Controller\IndexController', 221 | 'action' => 'index', 222 | 'default' => true 223 | ])->get('/hello/:name', [ 224 | 'controller' => 'MyApp\Controller\IndexController', 225 | 'action' => 'hello' 226 | ]); 227 | 228 | $app->run(); 229 | ``` 230 | 231 | The `default` parameter sets the controller as the default controller to handle 232 | routes that aren't found. Typically, there is a default action in the controller, 233 | such as an `error` method, to handle this. 234 | 235 | [Top](#popcorn-php-micro-framework) 236 | 237 | Custom Methods 238 | -------------- 239 | 240 | If your web server allows the configuration of custom HTTP methods, Popcorn 241 | supports that and allows you to register custom HTTP methods with the application. 242 | 243 | ```php 244 | use Popcorn\Pop; 245 | 246 | $app = new Pop(); 247 | $app->addCustomMethod('PURGE') 248 | ->addCustomMethod('COPY'); 249 | 250 | $app->purge('/image/:id', function(){ 251 | // Do something with the PURGE method on the image URL 252 | }); 253 | 254 | $app->copy('/image/:id', function(){ 255 | // Do something with the COPY method on the image URL 256 | }); 257 | 258 | $app->run(); 259 | ``` 260 | 261 | Then you can submit requests with your custom HTTP methods like this: 262 | 263 | ```bash 264 | $ curl -X PURGE http://localhost/image/1 265 | ``` 266 | ```bash 267 | $ curl -X COPY http://localhost/image/1 268 | ``` 269 | 270 | [Top](#popcorn-php-micro-framework) 271 | -------------------------------------------------------------------------------- /src/Pop.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2009-2026 NOLA Interactive, LLC. 8 | * @license https://www.popphp.org/license New BSD License 9 | */ 10 | 11 | /** 12 | * @namespace 13 | */ 14 | namespace Popcorn; 15 | 16 | use Pop\Application; 17 | 18 | /** 19 | * This is the main class for the Popcorn Micro-Framework. 20 | * 21 | * @category Popcorn 22 | * @package Popcorn 23 | * @author Nick Sagona, III 24 | * @copyright Copyright (c) 2009-2026 NOLA Interactive, LLC. 25 | * @license https://www.popphp.org/license New BSD License 26 | * @version 4.1.4 27 | */ 28 | class Pop extends Application 29 | { 30 | 31 | /** 32 | * Routes array 33 | * @var array 34 | */ 35 | protected array $routes = [ 36 | 'get' => [], 37 | 'head' => [], 38 | 'post' => [], 39 | 'put' => [], 40 | 'delete' => [], 41 | 'trace' => [], 42 | 'options' => [], 43 | 'connect' => [], 44 | 'patch' => [] 45 | ]; 46 | 47 | /** 48 | * Constructor 49 | * 50 | * Instantiate an application object 51 | * 52 | * Optional parameters are a service locator instance, a router instance, 53 | * an event manager instance or a configuration object or array 54 | * 55 | * @throws Exception 56 | */ 57 | public function __construct() 58 | { 59 | $args = func_get_args(); 60 | 61 | foreach ($args as $i => $arg) { 62 | if (is_array($arg)) { 63 | // Handle custom methods config 64 | if (isset($arg['custom_methods'])) { 65 | if (is_array($arg['custom_methods'])) { 66 | $this->addCustomMethods($arg['custom_methods']); 67 | } else { 68 | $this->addCustomMethod($arg['custom_methods']); 69 | } 70 | unset($args[$i]['custom_methods']); 71 | } 72 | 73 | // Handle routes config 74 | if (isset($arg['routes'])) { 75 | // Check for combined route matches 76 | foreach ($arg['routes'] as $key => $value) { 77 | // Handle route wildcard 78 | if ($key == '*') { 79 | foreach ($arg['routes'][$key] as $route => $controller) { 80 | $this->addToAll($route, $controller); 81 | } 82 | unset($arg['routes'][$key]); 83 | // Handle multiple route methods 84 | } else if (str_contains((string)$key, ',')) { 85 | foreach ($arg['routes'][$key] as $route => $controller) { 86 | $this->setRoutes($key, $route, $controller); 87 | } 88 | unset($arg['routes'][$key]); 89 | // Handle route prefixes 90 | } else if (str_starts_with($key, '/') && is_array($value)) { 91 | foreach ($value as $methods => $methodRoutes) { 92 | foreach ($methodRoutes as $route => $controller) { 93 | $this->setRoutes($methods, $key . $route, $controller); 94 | } 95 | } 96 | unset($arg['routes'][$key]); 97 | } 98 | } 99 | 100 | // Check for direct route method matches 101 | $routeKeys = array_keys($this->routes); 102 | foreach ($routeKeys as $key) { 103 | if (isset($arg['routes'][$key])) { 104 | foreach ($arg['routes'][$key] as $route => $controller) { 105 | $this->setRoute($key, $route, $controller); 106 | } 107 | unset($arg['routes'][$key]); 108 | } 109 | } 110 | 111 | // Check for static routes that are not assigned to a method, 112 | // and auto-assign them to get,post for a fallback 113 | if (count($arg['routes']) > 0) { 114 | foreach ($arg['routes'] as $route => $controller) { 115 | $this->setRoutes('get,post', $route, $controller); 116 | } 117 | } 118 | 119 | unset($args[$i]['routes']); 120 | } 121 | } 122 | } 123 | 124 | switch (count($args)) { 125 | case 1: 126 | parent::__construct($args[0]); 127 | break; 128 | case 2: 129 | parent::__construct($args[0], $args[1]); 130 | break; 131 | case 3: 132 | parent::__construct($args[0], $args[1], $args[2]); 133 | break; 134 | case 4: 135 | parent::__construct($args[0], $args[1], $args[2], $args[3]); 136 | break; 137 | case 5: 138 | parent::__construct($args[0], $args[1], $args[2], $args[3], $args[4]); 139 | break; 140 | case 6: 141 | parent::__construct($args[0], $args[1], $args[2], $args[3], $args[4], $args[5]); 142 | break; 143 | default: 144 | parent::__construct(); 145 | } 146 | 147 | } 148 | 149 | /** 150 | * Add a GET route 151 | * 152 | * @param string $route 153 | * @param mixed $controller 154 | * @throws Exception 155 | * @return Pop 156 | */ 157 | public function get(string $route, mixed $controller): Pop 158 | { 159 | return $this->setRoute('get', $route, $controller); 160 | } 161 | 162 | /** 163 | * Add a HEAD route 164 | * 165 | * @param string $route 166 | * @param mixed $controller 167 | * @throws Exception 168 | * @return Pop 169 | */ 170 | public function head(string $route, mixed $controller): Pop 171 | { 172 | return $this->setRoute('head', $route, $controller); 173 | } 174 | 175 | /** 176 | * Add a POST route 177 | * 178 | * @param string $route 179 | * @param mixed $controller 180 | * @throws Exception 181 | * @return Pop 182 | */ 183 | public function post(string $route, mixed $controller): Pop 184 | { 185 | return $this->setRoute('post', $route, $controller); 186 | } 187 | 188 | /** 189 | * Add a PUT route 190 | * 191 | * @param string $route 192 | * @param mixed $controller 193 | * @throws Exception 194 | * @return Pop 195 | */ 196 | public function put(string $route, mixed $controller): Pop 197 | { 198 | return $this->setRoute('put', $route, $controller); 199 | } 200 | 201 | /** 202 | * Add a DELETE route 203 | * 204 | * @param string $route 205 | * @param mixed $controller 206 | * @throws Exception 207 | * @return Pop 208 | */ 209 | public function delete(string $route, mixed $controller): Pop 210 | { 211 | return $this->setRoute('delete', $route, $controller); 212 | } 213 | 214 | /** 215 | * Add a TRACE route 216 | * 217 | * @param string $route 218 | * @param mixed $controller 219 | * @throws Exception 220 | * @return Pop 221 | */ 222 | public function trace(string $route, mixed $controller): Pop 223 | { 224 | return $this->setRoute('trace', $route, $controller); 225 | } 226 | 227 | /** 228 | * Add an OPTIONS route 229 | * 230 | * @param string $route 231 | * @param mixed $controller 232 | * @throws Exception 233 | * @return Pop 234 | */ 235 | public function options(string $route, mixed $controller): Pop 236 | { 237 | return $this->setRoute('options', $route, $controller); 238 | } 239 | 240 | /** 241 | * Add a CONNECT route 242 | * 243 | * @param string $route 244 | * @param mixed $controller 245 | * @throws Exception 246 | * @return Pop 247 | */ 248 | public function connect(string $route, mixed $controller): Pop 249 | { 250 | return $this->setRoute('connect', $route, $controller); 251 | } 252 | 253 | /** 254 | * Add a PATCH route 255 | * 256 | * @param string $route 257 | * @param mixed $controller 258 | * @throws Exception 259 | * @return Pop 260 | */ 261 | public function patch(string $route, mixed $controller): Pop 262 | { 263 | return $this->setRoute('patch', $route, $controller); 264 | } 265 | 266 | /** 267 | * Add to any and all methods (alias method to addToAll) 268 | * 269 | * @param string $route 270 | * @param mixed $controller 271 | * @return Pop 272 | */ 273 | public function any(string $route, mixed $controller): Pop 274 | { 275 | return $this->addToAll($route, $controller); 276 | } 277 | 278 | /** 279 | * Add a custom method 280 | * 281 | * @param string $customMethod 282 | * @return Pop 283 | */ 284 | public function addCustomMethod(string $customMethod): Pop 285 | { 286 | $this->routes[strtolower($customMethod)] = []; 287 | return $this; 288 | } 289 | 290 | /** 291 | * Add custom methods 292 | * 293 | * @param array $customMethods 294 | * @return Pop 295 | */ 296 | public function addCustomMethods(array $customMethods): Pop 297 | { 298 | foreach ($customMethods as $customMethod) { 299 | $this->addCustomMethod($customMethod); 300 | } 301 | return $this; 302 | } 303 | 304 | /** 305 | * Has a custom method 306 | * 307 | * @param string $customMethod 308 | * @return bool 309 | */ 310 | public function hasCustomMethod(string $customMethod): bool 311 | { 312 | return isset($this->routes[strtolower($customMethod)]); 313 | } 314 | 315 | /** 316 | * Add a route 317 | * 318 | * @param string $method 319 | * @param string $route 320 | * @param mixed $controller 321 | * @throws Exception 322 | * @return Pop 323 | */ 324 | public function setRoute(string $method, string $route, mixed $controller): Pop 325 | { 326 | if (!array_key_exists(strtolower((string)$method), $this->routes)) { 327 | throw new Exception("Error: The method '" . $method . "' is not allowed."); 328 | } 329 | 330 | if (is_callable($controller)) { 331 | $controller = ['controller' => $controller]; 332 | } 333 | 334 | if (isset($this->routes[$method][$route]) && is_array($this->routes[$method][$route])) { 335 | $this->routes[$method][$route] = array_merge($this->routes[$method][$route], $controller); 336 | } else { 337 | $this->routes[$method][$route] = $controller; 338 | } 339 | 340 | return $this; 341 | } 342 | 343 | /** 344 | * Add multiple routes 345 | * 346 | * @param array|string $methods 347 | * @param string $route 348 | * @param mixed $controller 349 | * @throws Exception 350 | * @return Pop 351 | */ 352 | public function setRoutes(array|string $methods, string $route, mixed $controller): Pop 353 | { 354 | if (is_string($methods)) { 355 | $methods = array_map('trim', explode(',', strtolower($methods))); 356 | } 357 | 358 | foreach ($methods as $method) { 359 | $this->setRoute($method, $route, $controller); 360 | } 361 | return $this; 362 | } 363 | 364 | /** 365 | * Add to all methods 366 | * 367 | * @param string $route 368 | * @param mixed $controller 369 | * @throws Exception 370 | * @return Pop 371 | */ 372 | public function addToAll(string $route, mixed $controller): Pop 373 | { 374 | foreach ($this->routes as $method => $value) { 375 | $this->setRoute($method, $route, $controller); 376 | } 377 | return $this; 378 | } 379 | 380 | /** 381 | * Method to get all routes 382 | * 383 | * @param ?string $method 384 | * @throws Exception 385 | * @return array 386 | */ 387 | public function getRoutes(?string $method = null): array 388 | { 389 | if (($method !== null) && !array_key_exists(strtolower($method), $this->routes)) { 390 | throw new Exception("Error: The method '" . strtoupper($method) . "' is not allowed."); 391 | } 392 | return ($method !== null) ? $this->routes[$method] : $this->routes; 393 | } 394 | 395 | /** 396 | * Method to get a route by method 397 | * 398 | * @param string $method 399 | * @param string $route 400 | * @return mixed 401 | */ 402 | public function getRoute(string $method, string $route): mixed 403 | { 404 | return ($this->hasRoute($method, $route)) ? $this->routes[$method][$route] : null; 405 | } 406 | 407 | /** 408 | * Method to determine if the application has a route 409 | * 410 | * @param string $method 411 | * @param string $route 412 | * @return bool 413 | */ 414 | public function hasRoute(string $method, string $route): bool 415 | { 416 | return (isset($this->routes[$method]) && isset($this->routes[$method][$route])); 417 | } 418 | 419 | /** 420 | * Determine if the route is allowed on for the method 421 | * 422 | * @param ?string $route 423 | * @return bool 424 | */ 425 | public function isAllowed(?string $route = null): bool 426 | { 427 | $allowed = false; 428 | $method = strtolower($_SERVER['REQUEST_METHOD']); 429 | $route = (string)$route; 430 | 431 | foreach ($this->routes[$method] as $rte => $ctrl) { 432 | if (is_array($ctrl) && !isset($ctrl['controller'])) { 433 | foreach ($ctrl as $r => $c) { 434 | if (str_starts_with($rte . $r, $route)) { 435 | $allowed = true; 436 | break; 437 | } 438 | } 439 | } else if (str_starts_with($rte, $route)) { 440 | $allowed = true; 441 | break; 442 | } 443 | } 444 | 445 | return $allowed; 446 | } 447 | 448 | /** 449 | * Run the application. 450 | * 451 | * @param bool $exit 452 | * @param ?string $forceRoute 453 | * @throws Exception|\Pop\Event\Exception|\Pop\Router\Exception|\ReflectionException 454 | * @return void 455 | */ 456 | public function run(bool $exit = true, ?string $forceRoute = null): void 457 | { 458 | // If method is not allowed 459 | if (!isset($this->routes[strtolower((string)$_SERVER['REQUEST_METHOD'])])) { 460 | throw new Exception( 461 | "Error: The method '" . strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' is not allowed.", 405 462 | ); 463 | } 464 | 465 | // Route request 466 | $this->router->addRoutes($this->routes[strtolower((string)$_SERVER['REQUEST_METHOD'])]); 467 | $this->router->route(); 468 | 469 | // If route is allowed for this method 470 | if ($this->router->hasRoute() && $this->isAllowed($this->router->getRouteMatch()->getOriginalRoute())) { 471 | parent::run($exit, $forceRoute); 472 | // Else, handle error 473 | } else { 474 | if ($this->router->hasRoute()) { 475 | $message = "Error: The route '" . $_SERVER['REQUEST_URI'] . 476 | "' is not allowed on the '" . strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' method"; 477 | } else { 478 | $message = "Error: That route '" . $_SERVER['REQUEST_URI'] . "' was not found for the '" . 479 | strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' method"; 480 | } 481 | 482 | $this->trigger('app.error', ['exception' => new Exception($message, 404)]); 483 | $this->router->getRouteMatch()->noRouteFound((bool)$exit); 484 | } 485 | } 486 | 487 | /** 488 | * Magic method to check for a custom method 489 | * 490 | * @param string $methodName 491 | * @param array $arguments 492 | * @throws Exception 493 | * @return void 494 | */ 495 | public function __call(string $methodName, array $arguments): void 496 | { 497 | if (!isset($this->routes[strtolower($methodName)])) { 498 | throw new Exception("Error: The custom method '" . strtoupper($methodName) . "' is not allowed."); 499 | } 500 | 501 | if (count($arguments) != 2) { 502 | throw new Exception("Error: You must pass a route and a controller."); 503 | } 504 | 505 | [$route, $controller] = $arguments; 506 | 507 | $this->setRoute(strtolower((string)$methodName), $route, $controller); 508 | } 509 | 510 | } 511 | --------------------------------------------------------------------------------