Compare commits

..

3 Commits

Author SHA1 Message Date
Simon Sessingø 2952f6a3b6 Added \JsonSerializable interface to Response->json (issue: #284). 2017-08-31 11:59:58 +01:00
Simon Sessingø 16e326ad9f Development
- all() in Input class now returns correct array.
- all() now supports json data.
- Minor bugfixes.
2016-11-08 18:16:17 +02:00
Simon Sessingø d9b2328e82 Fixed RouterGroup not pushing multiple middlewares properly 2016-11-07 04:56:47 +01:00
119 changed files with 2910 additions and 11092 deletions
-59
View File
@@ -1,59 +0,0 @@
name: CI
on: [push, pull_request]
jobs:
build-test:
runs-on: ${{ matrix.os }}
env:
PHP_EXTENSIONS: json
PHP_INI_VALUES: assert.exception=1, zend.assertions=1
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
php-version:
- 7.4
- 8.0
phpunit-version:
- 8.5.32
dependencies:
- lowest
- highest
name: PHPUnit Tests
steps:
- name: Configure git to avoid issues with line endings
if: matrix.os == 'windows-latest'
run: git config --global core.autocrlf false
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer:v5, phpunit:${{ matrix.phpunit-versions }}
coverage: xdebug
extensions: ${{ env.PHP_EXTENSIONS }}
ini-values: ${{ env.PHP_INI_VALUES }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
restore-keys: |
php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
- name: Install lowest dependencies with composer
if: matrix.dependencies == 'lowest'
run: composer update --no-ansi --no-interaction --no-progress --prefer-lowest
- name: Install highest dependencies with composer
if: matrix.dependencies == 'highest'
run: composer update --no-ansi --no-interaction --no-progress
- name: Run tests with phpunit
run: composer test
+2 -2
View File
@@ -1,4 +1,4 @@
.idea
composer.lock
vendor/
.idea/
.phpunit.result.cache
demo-project/vendor
+254 -1883
View File
File diff suppressed because it is too large Load Diff
+23 -53
View File
@@ -1,56 +1,26 @@
{
"name": "pecee/simple-router",
"description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.",
"keywords": [
"router",
"router",
"routing",
"route",
"simple-php-router",
"laravel",
"pecee",
"php",
"framework",
"url-handling",
"input-handler",
"routing-engine",
"request-handler"
],
"license": "MIT",
"support": {
"source": "https://github.com/skipperbent/simple-php-router/issues"
},
"authors": [
{
"name": "Simon Sessingø",
"email": "simon.sessingoe@gmail.com"
"name": "pecee/simple-router",
"description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.",
"keywords": [ "router", "routing", "laravel", "pecee" ],
"license": "MIT",
"support": {
"source": "https://github.com/skipperbent/simple-php-router/issues"
},
"authors": [
{
"name": "Simon Sessingø",
"email": "simon.sessingoe@gmail.com"
}
],
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "4.7.7"
},
"autoload": {
"psr-4": {
"Pecee\\": "src/Pecee/"
}
}
],
"require": {
"php": ">=7.4",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^8",
"mockery/mockery": "^1",
"phpstan/phpstan": "^0",
"phpstan/phpstan-phpunit": "^0",
"phpstan/phpstan-deprecation-rules": "^0",
"phpstan/phpstan-strict-rules": "^0"
},
"scripts": {
"test": [
"phpunit tests"
]
},
"autoload": {
"psr-4": {
"Pecee\\": "src/Pecee/"
}
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true
}
}
}
+100
View File
@@ -0,0 +1,100 @@
# Simple PHP router demo project
This project is here to give you a basic understanding of how to setup and using simple-php-router.
Please note that this demo-project only covers how to integrate the `simple-php-router` in a project without a framework. If you are using some sort of PHP framework in your project the implementation might vary.
**What we won't cover:**
- How to setup a solution that fits your need. This is a basic demo to help you get started.
- Understanding of MVC; including Controllers, Middlewares or ExceptionHandlers.
- How to integrate into third party frameworks.
**What we cover:**
- How to get up and running fast - from scratch.
- How to get ExceptionHandlers, Middlewares and Controllers working.
- How to setup your webservers.
## Installation
- Navigate to the `demo-project` folder in terminal and run `composer update` to install the latest version.
- Point your webserver to `demo-project/public`.
### Setting up Nginx
If you are using Nginx please make sure that url-rewriting is enabled.
You can easily enable url-rewriting by adding the following configuration for the Nginx configuration-file for the demo-project.
```
location / {
try_files $uri $uri/ /index.php?$query_string;
}
```
### Setting up Apache
Nothing special is required for Apache to work. We've include the `.htaccess` file in the `public` folder. If rewriting is not working for you, please check that the `mod_rewrite` module (htaccess support) is enabled in the Apache configuration.
## Folder structure
| Folder | Description |
| ------------- |-------------|
| app |Contains projects-specific PHP classes|
| public |Public folder which are accessible through the web.|
## Notes
The demo project has it's own `Router` class implementation which extends the `SimpleRouter` class with further functionality.
This class can be useful adding additional functionality that are required before and after routing occurs or any extra functionality belonging to the router itself.
In this project we also use our custom router-class to autoload the `routes.php` file from our custom location (`app/routes.php`).
Please check the `routes.php` file in `demo-project/app` for all the urls/rules available in the project.
### CSRF-verifier
For the purpose of this demo, we've added a custom CSRF-verifier middleware called `CsrfVerifier` and disabled CSRF checks for all calls to `/api/*`. This will ensure that CSRF form-checks are not applied when calling our demo api url.
### Exception handlers
The included `CustomExceptionHandler` class returns a very basic json response for errors received on calls to `/api/*` or otherwise just a simple formatted error response.
### Middlewares
`ApiVerification` class is added to all calls to `/api/*`. This simple class just adds some data to the `Request` object, which is returned in one of the methods in the `ApiController` class. We've added this class to demonstrate that you can use middlewares to ensure that the user has the correct authentication - before router loads the controller itself.
### Urls
Please see `routes.php` for all routes and rules.
| URL |
| ------------- |
| / |
| /api/demo |
| /companies |
| /companies/[id] |
| /contact |
## The MIT License (MIT)
Copyright (c) 2016 Simon Sessingø / simple-php-router
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,22 @@
<?php
namespace Demo\Controllers;
use Pecee\Http\Request;
class ApiController {
public function index() {
// The variable authenticated is set to true in the ApiVerification middleware class.
$request = Request::getInstance();
header('content-type: application/json');
echo json_encode([
'authenticated' => $request->authenticated
]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Demo\Controllers;
class DefaultController {
public function index() {
// implement
echo 'DefaultController -> index';
}
public function contact() {
echo 'DefaultController -> contact';
}
public function companies($id = null) {
echo 'DefaultController -> companies -> id: ' . $id;
}
public function notFound() {
echo 'Page not found';
}
}
@@ -0,0 +1,34 @@
<?php
namespace Demo\Handlers;
use Pecee\Handler\IExceptionHandler;
use Pecee\Http\Request;
use Pecee\SimpleRouter\RouterEntry;
class CustomExceptionHandler implements IExceptionHandler {
public function handleError( Request $request, RouterEntry $router = null, \Exception $error) {
// Return json errors if we encounter an error on /api.
if(stripos($request->getUri(), '/api') !== false) {
header('content-type: application/json');
echo json_encode([
'error' => $error->getMessage(),
'code' => $error->getCode()
]);
die();
}
// else we just throw the error
if($error->getCode() == 404) {
// Return 404 path
$request->setUri('/404');
return $request;
}
throw $error;
}
}
@@ -0,0 +1,16 @@
<?php
namespace Demo\Middlewares;
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
class ApiVerification implements IMiddleware {
public function handle(Request $request) {
// Do authentication
$request->authenticated = true;
}
}
@@ -0,0 +1,13 @@
<?php
namespace Demo\Middlewares;
use Pecee\Http\Middleware\BaseCsrfVerifier;
class CsrfVerifier extends BaseCsrfVerifier {
/**
* CSRF validation will be ignored on the following urls.
*/
protected $except = ['/api/*'];
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* Custom router which handles default middlewares, default exceptions and things
* that should be happen before and after the router is initialised.
*/
namespace Demo;
use Pecee\SimpleRouter\SimpleRouter;
class Router extends SimpleRouter {
public static function start($defaultNamespace = null) {
// change this to whatever makes sense in your project
require_once 'routes.php';
// Do initial stuff
parent::start('\\Demo\\Controllers');
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
/**
* This file contains all the routes for the project
*/
use Demo\Router;
Router::csrfVerifier(new \Demo\Middlewares\CsrfVerifier());
Router::group(['exceptionHandler' => 'Demo\Handlers\CustomExceptionHandler'], function() {
Router::get('/', 'DefaultController@index')->setAlias('home');
Router::get('/contact', 'DefaultController@contact')->setAlias('contact');
Router::get('/404', 'DefaultController@notFound')->setAlias('404');
Router::basic('/companies', 'DefaultController@companies')->setAlias('companies');
Router::basic('/companies/{id}', 'DefaultController@companies')->setAlias('companies');
// Api
Router::group(['prefix' => '/api', 'middleware' => 'Demo\Middlewares\ApiVerification'], function() {
Router::resource('/demo', 'ApiController');
});
});
+26
View File
@@ -0,0 +1,26 @@
{
"name": "pecee/simple-router-demo",
"description": "Simple router demo project",
"keywords": [
"simple-router",
"php",
"php-simple-router"
],
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.4.0",
"pecee/simple-router": "1.*"
},
"require-dev": {
},
"config": {
"preferred-install": "dist"
},
"autoload": {
"psr-4": {
"Demo\\": "app/"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
RewriteEngine on
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-l
RewriteRule ^(.*)$ index.php/$1
+7
View File
@@ -0,0 +1,7 @@
<?php
// load composer dependencies
require '../vendor/autoload.php';
// Start the routing
\Demo\Router::start();
-88
View File
@@ -1,88 +0,0 @@
<?php
use Pecee\SimpleRouter\SimpleRouter as Router;
use Pecee\Http\Url;
use Pecee\Http\Response;
use Pecee\Http\Request;
/**
* Get url for a route by using either name/alias, class or method name.
*
* The name parameter supports the following values:
* - Route name
* - Controller/resource name (with or without method)
* - Controller class name
*
* When searching for controller/resource by name, you can use this syntax "route.name@method".
* You can also use the same syntax when searching for a specific controller-class "MyController@home".
* If no arguments is specified, it will return the url for the current loaded route.
*
* @param string|null $name
* @param string|array|null $parameters
* @param array|null $getParams
* @return \Pecee\Http\Url
* @throws \InvalidArgumentException
*/
function url(?string $name = null, $parameters = null, ?array $getParams = null): Url
{
return Router::getUrl($name, $parameters, $getParams);
}
/**
* @return \Pecee\Http\Response
*/
function response(): Response
{
return Router::response();
}
/**
* @return \Pecee\Http\Request
*/
function request(): Request
{
return Router::request();
}
/**
* Get input class
* @param string|null $index Parameter index name
* @param string|mixed|null $defaultValue Default return value
* @param array ...$methods Default methods
* @return \Pecee\Http\Input\InputHandler|array|string|null
*/
function input($index = null, $defaultValue = null, ...$methods)
{
if ($index !== null) {
return request()->getInputHandler()->value($index, $defaultValue, ...$methods);
}
return request()->getInputHandler();
}
/**
* @param string $url
* @param int|null $code
*/
function redirect(string $url, ?int $code = null): void
{
if ($code !== null) {
response()->httpCode($code);
}
response()->redirect($url);
}
/**
* Get current csrf-token
* @return string|null
*/
function csrf_token(): ?string
{
$baseVerifier = Router::router()->getCsrfVerifier();
if ($baseVerifier !== null) {
return $baseVerifier->getTokenProvider()->getToken();
}
return null;
}
-22
View File
@@ -1,22 +0,0 @@
parameters:
level: 6
paths:
- src
fileExtensions:
- php
bootstrapFiles:
- ./vendor/autoload.php
ignoreErrors:
reportUnmatchedIgnoredErrors: true
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
parallel:
processTimeout: 300.0
jobSize: 10
maximumNumberOfProcesses: 4
minimumNumberOfJobsPerProcess: 4
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
-24
View File
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
bootstrap="tests/bootstrap.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="SimpleRouter Test Suite">
<directory>tests/Pecee/SimpleRouter/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true"
processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>
@@ -1,48 +0,0 @@
<?php
namespace Pecee\Controllers;
interface IResourceController
{
/**
* @return mixed
*/
public function index();
/**
* @param mixed $id
* @return mixed
*/
public function show($id);
/**
* @return mixed
*/
public function store();
/**
* @return mixed
*/
public function create();
/**
* View
* @param mixed $id
* @return mixed
*/
public function edit($id);
/**
* @param mixed $id
* @return mixed
*/
public function update($id);
/**
* @param mixed $id
* @return mixed
*/
public function destroy($id);
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace Pecee;
class CsrfToken {
const CSRF_KEY = 'XSRF-TOKEN';
protected $token;
/**
* Generate random identifier for CSRF token
* @return string
*/
public static function generateToken() {
if (function_exists('mcrypt_create_iv')) {
return bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
}
return bin2hex(openssl_random_pseudo_bytes(32));
}
/**
* Validate valid CSRF token
*
* @param string $token
* @return bool
*/
public function validate($token) {
if($token !== null && $this->getToken() !== null) {
return hash_equals($token, $this->getToken());
}
return false;
}
/**
* Set csrf token cookie
*
* @param $token
*/
public function setToken($token) {
setcookie(static::CSRF_KEY, $token, time() + 60 * 120, '/');
}
/**
* Get csrf token
* @return string|null
*/
public function getToken(){
if($this->hasToken()) {
return $_COOKIE[static::CSRF_KEY];
}
return null;
}
/**
* Returns whether the csrf token has been defined
* @return bool
*/
public function hasToken() {
return isset($_COOKIE[static::CSRF_KEY]);
}
}
+3
View File
@@ -0,0 +1,3 @@
<?php
namespace Pecee\Exception;
class RouterException extends \Exception { }
@@ -0,0 +1,4 @@
<?php
namespace Pecee\Exception;
class TokenMismatchException extends \Exception {}
@@ -1,8 +0,0 @@
<?php
namespace Pecee\Exceptions;
class InvalidArgumentException extends \InvalidArgumentException
{
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Pecee\Handler;
use Pecee\Http\Request;
use Pecee\SimpleRouter\RouterEntry;
interface IExceptionHandler {
public function handleError(Request $request, RouterEntry $router = null, \Exception $error);
}
@@ -1,10 +0,0 @@
<?php
namespace Pecee\Http\Exceptions;
use Exception;
class MalformedUrlException extends Exception
{
}
-28
View File
@@ -1,28 +0,0 @@
<?php
namespace Pecee\Http\Input;
interface IInputItem
{
public function getIndex(): string;
public function setIndex(string $index): self;
public function getName(): ?string;
public function setName(string $name): self;
/**
* @return mixed
*/
public function getValue();
/**
* @param mixed $value
*/
public function setValue($value): self;
public function __toString(): string;
}
+213
View File
@@ -0,0 +1,213 @@
<?php
namespace Pecee\Http\Input;
use Pecee\Http\Request;
class Input {
/**
* @var \Pecee\Http\Input\InputCollection
*/
public $get;
/**
* @var \Pecee\Http\Input\InputCollection
*/
public $post;
/**
* @var \Pecee\Http\Input\InputCollection
*/
public $file;
/**
* @var Request
*/
protected $request;
public function __construct(Request &$request) {
$this->request = $request;
$this->setGet();
$this->setPost();
$this->setFile();
}
/**
* Get all get/post items
* @param array|null $filter Only take items in filter
* @return array
*/
public function all(array $filter = null) {
$output = $_POST;
if($this->request->getMethod() === 'post') {
$contents = file_get_contents('php://input');
if (stripos(trim($contents), '{') === 0) {
$output = json_decode($contents, true);
if($output === false) {
$output = array();
}
}
}
$output = array_merge($_GET, $output);
if($filter !== null) {
$output = array_filter($output, function ($key) use ($filter) {
if (in_array($key, $filter)) {
return true;
}
return false;
}, ARRAY_FILTER_USE_KEY);
}
return $output;
}
public function getObject($index, $default = null) {
$key = (strpos($index, '[') > -1) ? substr($index, strpos($index, '[')+1, strpos($index, ']') - strlen($index)) : null;
$index = (strpos($index, '[') > -1) ? substr($index, 0, strpos($index, '[')) : $index;
$element = $this->get->findFirst($index);
if($element !== null) {
return ($key !== null) ? $element[$key] : $element;
}
if($this->request->getMethod() !== 'get') {
$element = $this->post->findFirst($index);
if ($element !== null) {
return ($key !== null) ? $element[$key] : $element;
}
$element = $this->file->findFirst($index);
if ($element !== null) {
return ($key !== null) ? $element[$key] : $element;
}
}
return $default;
}
/**
* Get input element value matching index
* @param string $index
* @param string|null $default
* @return string|null
*/
public function get($index, $default = null) {
$item = $this->getObject($index);
if($item !== null) {
if($item instanceof InputCollection || $item instanceof InputFile) {
return $item;
}
return (trim($item->getValue()) === '') ? $default : $item->getValue();
}
return $default;
}
public function exists($index) {
return ($this->getObject($index) !== null);
}
public function setGet() {
$this->get = new InputCollection();
if(count($_GET)) {
foreach($_GET as $key => $get) {
if(!is_array($get)) {
$this->get->{$key} = new InputItem($key, $get);
continue;
}
$output = new InputCollection();
foreach($get as $k => $g) {
$output->{$k} = new InputItem($k, $g);
}
$this->get->{$key} = $output;
}
}
}
public function setPost() {
$this->post = new InputCollection();
$postVars = $_POST;
if(in_array($this->request->getMethod(), ['put', 'patch', 'delete'])) {
parse_str(file_get_contents('php://input'), $postVars);
}
if(count($postVars)) {
foreach($postVars as $key => $post) {
if(!is_array($post)) {
$this->post->{strtolower($key)} = new InputItem($key, $post);
continue;
}
$output = new InputCollection();
foreach($post as $k => $p) {
$output->{$k} = new InputItem($k, $p);
}
$this->post->{strtolower($key)} = $output;
}
}
}
public function setFile() {
$this->file = new InputCollection();
if(count($_FILES)) {
foreach($_FILES as $key => $value) {
// Multiple files
if(!is_array($value['name'])) {
// Strip empty values
if($value['error'] != '4') {
$file = new InputFile($key);
$file->setName($value['name']);
$file->setSize($value['size']);
$file->setType($value['type']);
$file->setTmpName($value['tmp_name']);
$file->setError($value['error']);
$this->file->{strtolower($key)} = $file;
}
continue;
}
$output = new InputCollection();
foreach($value['name'] as $k=>$val) {
// Strip empty values
if($value['error'][$k] != '4') {
$file = new InputFile($k);
$file->setName($value['name'][$k]);
$file->setSize($value['size'][$k]);
$file->setType($value['type'][$k]);
$file->setTmpName($value['tmp_name'][$k]);
$file->setError($value['error'][$k]);
$output->{$k} = $file;
}
}
$this->file->{strtolower($key)} = $output;
}
}
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace Pecee\Http\Input;
class InputCollection implements \IteratorAggregate {
protected $data = array();
/**
* Search for input element matching index.
* Useful for searching for finding items where $index doesn't contain form name.
*
* @param string $index
* @return mixed
*/
public function findFirst($index) {
if(count($this->data)) {
if(isset($this->data[$index])) {
return $this->data[$index];
}
foreach($this->data as $key => $value) {
if(strtolower($index) === strtolower($key)) {
return $value;
}
}
}
return null;
}
/**
* @param $index
* @throws \InvalidArgumentException
* @return InputItem
*/
public function __get($index) {
$item = $this->findFirst($index);
// Ensure that item are always available
if($item === null) {
$this->data[$index] = new InputItem($index, null);
return $this->data[$index];
}
return $item;
}
public function __set($index, $value) {
$this->data[$index] = $value;
}
public function getData() {
return $this->data;
}
/**
* Retrieve an external iterator
* @link http://php.net/manual/en/iteratoraggregate.getiterator.php
* @return \Traversable An instance of an object implementing <b>Iterator</b> or
* <b>Traversable</b>
* @since 5.0.0
*/
public function getIterator() {
return new \ArrayIterator($this->data);
}
}
+45 -296
View File
@@ -1,319 +1,68 @@
<?php
namespace Pecee\Http\Input;
use Pecee\Exceptions\InvalidArgumentException;
class InputFile extends InputItem {
class InputFile implements IInputItem
{
/**
* @var string
*/
public $index;
protected $name;
protected $size;
protected $type;
protected $error;
protected $tmpName;
/**
* @var string
*/
public $name;
/**
* @return string
*/
public function getSize() {
return $this->size;
}
/**
* @var string|null
*/
public $filename;
/**
* @return string
*/
public function getType() {
return $this->type;
}
/**
* @var int|null
*/
public $size;
/**
* @return string
*/
public function getError() {
return $this->error;
}
/**
* @var int|null
*/
public $type;
/**
* @return string
*/
public function getTmpName() {
return $this->tmpName;
}
/**
* @var int
*/
public $errors;
/**
* @var string|null
*/
public $tmpName;
public function __construct(string $index)
{
$this->index = $index;
$this->errors = 0;
// Make the name human friendly, by replace _ with space
$this->name = ucfirst(str_replace('_', ' ', strtolower($this->index)));
public function getExtension() {
return pathinfo($this->getName(), PATHINFO_EXTENSION);
}
/**
* Create from array
*
* @param array $values
* @throws InvalidArgumentException
* @return static
*/
public static function createFromArray(array $values): self
{
if (isset($values['index']) === false) {
throw new InvalidArgumentException('Index key is required');
}
public function move($destination) {
return move_uploaded_file($this->tmpName, $destination);
}
/* Easy way of ensuring that all indexes-are set and not filling the screen with isset() */
public function getContents() {
return file_get_contents($this->tmpName);
}
$values += [
'tmp_name' => null,
'type' => null,
'size' => null,
'name' => null,
'error' => null,
];
return (new static($values['index']))
->setSize((int)$values['size'])
->setError((int)$values['error'])
->setType($values['type'])
->setTmpName($values['tmp_name'])
->setFilename($values['name']);
}
/**
* @return string
*/
public function getIndex(): string
{
return $this->index;
}
/**
* Set input index
* @param string $index
* @return static
*/
public function setIndex(string $index): IInputItem
{
$this->index = $index;
return $this;
}
/**
* @return string
*/
public function getSize(): string
{
return $this->size;
}
/**
* Set file size
* @param int $size
* @return static
*/
public function setSize(int $size): IInputItem
{
$this->size = $size;
return $this;
}
/**
* Get mime-type of file
* @return string
*/
public function getMime(): string
{
return $this->getType();
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Set type
* @param string $type
* @return static
*/
public function setType(string $type): IInputItem
{
$this->type = $type;
return $this;
}
/**
* Returns extension without "."
*
* @return string
*/
public function getExtension(): string
{
return pathinfo($this->getFilename(), PATHINFO_EXTENSION);
}
/**
* Get human friendly name
*
* @return string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Set human friendly name.
* Useful for adding validation etc.
*
* @param string $name
* @return static
*/
public function setName(string $name): IInputItem
{
$this->name = $name;
return $this;
}
/**
* Set filename
*
* @param string $name
* @return static
*/
public function setFilename(string $name): IInputItem
{
$this->filename = $name;
return $this;
}
/**
* Get filename
*
* @return string mixed
*/
public function getFilename(): ?string
{
return $this->filename;
}
/**
* Move the uploaded temporary file to it's new home
*
* @param string $destination
* @return bool
*/
public function move(string $destination): bool
{
return move_uploaded_file($this->tmpName, $destination);
}
/**
* Get file contents
*
* @return string
*/
public function getContents(): string
{
return file_get_contents($this->tmpName);
}
/**
* Return true if an upload error occurred.
*
* @return bool
*/
public function hasError(): bool
{
return ($this->getError() !== 0);
}
/**
* Get upload-error code.
*
* @return int|null
*/
public function getError(): ?int
{
return $this->errors;
}
/**
* Set error
*
* @param int|null $error
* @return static
*/
public function setError(?int $error): IInputItem
{
$this->errors = (int)$error;
return $this;
}
/**
* @return string
*/
public function getTmpName(): string
{
return $this->tmpName;
}
/**
* Set file temp. name
* @param string $name
* @return static
*/
public function setTmpName(string $name): IInputItem
{
public function setTmpName($name) {
$this->tmpName = $name;
return $this;
}
public function __toString(): string
{
return $this->getTmpName();
public function setSize($size) {
$this->size = $size;
}
public function getValue(): string
{
return $this->getFilename();
public function setType($type) {
$this->type = $type;
}
/**
* @param mixed $value
* @return static
*/
public function setValue($value): IInputItem
{
$this->filename = $value;
return $this;
}
public function toArray(): array
{
return [
'tmp_name' => $this->tmpName,
'type' => $this->type,
'size' => $this->size,
'name' => $this->name,
'error' => $this->errors,
'filename' => $this->filename,
];
public function setError($error) {
$this->error = $error;
}
}
-470
View File
@@ -1,470 +0,0 @@
<?php
namespace Pecee\Http\Input;
use Pecee\Exceptions\InvalidArgumentException;
use Pecee\Http\Request;
class InputHandler
{
/**
* @var array
*/
protected $get = [];
/**
* @var array
*/
protected $post = [];
/**
* @var array
*/
protected $file = [];
/**
* @var Request
*/
protected $request;
/**
* Original post variables
* @var array
*/
protected $originalPost = [];
/**
* Original get/params variables
* @var array
*/
protected $originalParams = [];
/**
* Get original file variables
* @var array
*/
protected $originalFile = [];
/**
* Input constructor.
* @param Request $request
*/
public function __construct(Request $request)
{
$this->request = $request;
$this->parseInputs();
}
/**
* Parse input values
*
*/
public function parseInputs(): void
{
/* Parse get requests */
if (count($_GET) !== 0) {
$this->originalParams = $_GET;
$this->get = $this->parseInputItem($this->originalParams);
}
/* Parse post requests */
$this->originalPost = $_POST;
if ($this->request->isPostBack() === true) {
$contents = file_get_contents('php://input');
// Append any PHP-input json
if (strpos(trim($contents), '{') === 0) {
$post = json_decode($contents, true);
if ($post !== false) {
$this->originalPost += $post;
}
}
}
if (count($this->originalPost) !== 0) {
$this->post = $this->parseInputItem($this->originalPost);
}
/* Parse get requests */
if (count($_FILES) !== 0) {
$this->originalFile = $_FILES;
$this->file = $this->parseFiles($this->originalFile);
}
}
/**
* @param array $files Array with files to parse
* @param string|null $parentKey Key from parent (used when parsing nested array).
* @return array
*/
public function parseFiles(array $files, ?string $parentKey = null): array
{
$list = [];
foreach ($files as $key => $value) {
// Parse multi dept file array
if(isset($value['name']) === false && is_array($value) === true) {
$list[$key] = $this->parseFiles($value, $key);
continue;
}
// Handle array input
if (is_array($value['name']) === false) {
$values = ['index' => $parentKey ?? $key];
try {
$list[$key] = InputFile::createFromArray($values + $value);
} catch (InvalidArgumentException $e) {
}
continue;
}
$keys = [$key];
$files = $this->rearrangeFile($value['name'], $keys, $value);
if (isset($list[$key]) === true) {
$list[$key][] = $files;
} else {
$list[$key] = $files;
}
}
return $list;
}
/**
* Rearrange multi-dimensional file object created by PHP.
*
* @param array $values
* @param array $index
* @param array|null $original
* @return array
*/
protected function rearrangeFile(array $values, array &$index, ?array $original): array
{
$originalIndex = $index[0];
array_shift($index);
$output = [];
foreach ($values as $key => $value) {
if (is_array($original['name'][$key]) === false) {
try {
$file = InputFile::createFromArray([
'index' => ($key === '' && $originalIndex !== '') ? $originalIndex : $key,
'name' => $original['name'][$key],
'error' => $original['error'][$key],
'tmp_name' => $original['tmp_name'][$key],
'type' => $original['type'][$key],
'size' => $original['size'][$key],
]);
if (isset($output[$key]) === true) {
$output[$key][] = $file;
continue;
}
$output[$key] = $file;
continue;
} catch (InvalidArgumentException $e) {
}
}
$index[] = $key;
$files = $this->rearrangeFile($value, $index, $original);
if (isset($output[$key]) === true) {
$output[$key][] = $files;
} else {
$output[$key] = $files;
}
}
return $output;
}
/**
* Parse input item from array
*
* @param array $array
* @return array
*/
protected function parseInputItem(array $array): array
{
$list = [];
foreach ($array as $key => $value) {
// Handle array input
if (is_array($value) === true) {
$value = $this->parseInputItem($value);
}
$list[$key] = new InputItem($key, $value);
}
return $list;
}
/**
* Find input object
*
* @param string $index
* @param array ...$methods
* @return IInputItem|array|null
*/
public function find(string $index, ...$methods)
{
$element = null;
if(count($methods) > 0) {
$methods = is_array(...$methods) ? array_values(...$methods) : $methods;
}
if (count($methods) === 0 || in_array(Request::REQUEST_TYPE_GET, $methods, true) === true) {
$element = $this->get($index);
}
if (($element === null && count($methods) === 0) || (count($methods) !== 0 && in_array(Request::REQUEST_TYPE_POST, $methods, true) === true)) {
$element = $this->post($index);
}
if (($element === null && count($methods) === 0) || (count($methods) !== 0 && in_array('file', $methods, true) === true)) {
$element = $this->file($index);
}
return $element;
}
protected function getValueFromArray(array $array): array
{
$output = [];
/* @var $item InputItem */
foreach ($array as $key => $item) {
if ($item instanceof IInputItem) {
$item = $item->getValue();
}
$output[$key] = is_array($item) ? $this->getValueFromArray($item) : $item;
}
return $output;
}
/**
* Get input element value matching index
*
* @param string $index
* @param string|mixed|null $defaultValue
* @param array ...$methods
* @return string|array
*/
public function value(string $index, $defaultValue = null, ...$methods)
{
$input = $this->find($index, ...$methods);
if ($input instanceof IInputItem) {
$input = $input->getValue();
}
/* Handle collection */
if (is_array($input) === true) {
$output = $this->getValueFromArray($input);
return (count($output) === 0) ? $defaultValue : $output;
}
return ($input === null || (is_string($input) && trim($input) === '')) ? $defaultValue : $input;
}
/**
* Check if a input-item exist.
* If an array is as $index parameter the method returns true if all elements exist.
*
* @param string|array $index
* @param array ...$methods
* @return bool
*/
public function exists($index, ...$methods): bool
{
// Check array
if(is_array($index) === true) {
foreach($index as $key) {
if($this->value($key, null, ...$methods) === null) {
return false;
}
}
return true;
}
return $this->value($index, null, ...$methods) !== null;
}
/**
* Find post-value by index or return default value.
*
* @param string $index
* @param mixed|null $defaultValue
* @return InputItem|array|string|null
*/
public function post(string $index, $defaultValue = null)
{
return $this->post[$index] ?? $defaultValue;
}
/**
* Find file by index or return default value.
*
* @param string $index
* @param mixed|null $defaultValue
* @return InputFile|array|string|null
*/
public function file(string $index, $defaultValue = null)
{
return $this->file[$index] ?? $defaultValue;
}
/**
* Find parameter/query-string by index or return default value.
*
* @param string $index
* @param mixed|null $defaultValue
* @return InputItem|array|string|null
*/
public function get(string $index, $defaultValue = null)
{
return $this->get[$index] ?? $defaultValue;
}
/**
* Get all get/post items
* @param array $filter Only take items in filter
* @return array
*/
public function all(array $filter = []): array
{
$output = $this->originalParams + $this->originalPost + $this->originalFile;
$output = (count($filter) > 0) ? array_intersect_key($output, array_flip($filter)) : $output;
foreach ($filter as $filterKey) {
if (array_key_exists($filterKey, $output) === false) {
$output[$filterKey] = null;
}
}
return $output;
}
/**
* Add GET parameter
*
* @param string $key
* @param InputItem $item
*/
public function addGet(string $key, InputItem $item): void
{
$this->get[$key] = $item;
}
/**
* Add POST parameter
*
* @param string $key
* @param InputItem $item
*/
public function addPost(string $key, InputItem $item): void
{
$this->post[$key] = $item;
}
/**
* Add FILE parameter
*
* @param string $key
* @param InputFile $item
*/
public function addFile(string $key, InputFile $item): void
{
$this->file[$key] = $item;
}
/**
* Get original post variables
* @return array
*/
public function getOriginalPost(): array
{
return $this->originalPost;
}
/**
* Set original post variables
* @param array $post
* @return static $this
*/
public function setOriginalPost(array $post): self
{
$this->originalPost = $post;
return $this;
}
/**
* Get original get variables
* @return array
*/
public function getOriginalParams(): array
{
return $this->originalParams;
}
/**
* Set original get-variables
* @param array $params
* @return static $this
*/
public function setOriginalParams(array $params): self
{
$this->originalParams = $params;
return $this;
}
/**
* Get original file variables
* @return array
*/
public function getOriginalFile(): array
{
return $this->originalFile;
}
/**
* Set original file posts variables
* @param array $file
* @return static $this
*/
public function setOriginalFile(array $file): self
{
$this->originalFile = $file;
return $this;
}
}
+29 -80
View File
@@ -1,114 +1,63 @@
<?php
namespace Pecee\Http\Input;
use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
class InputItem {
class InputItem implements ArrayAccess, IInputItem, IteratorAggregate
{
public $index;
public $name;
public $value;
protected $index;
protected $name;
protected $value;
public function __construct(string $index, $value = null)
{
public function __construct($index, $value = null) {
$this->index = $index;
$this->value = $value;
// Make the name human friendly, by replace _ with space
$this->name = ucfirst(str_replace('_', ' ', strtolower($this->index)));
$this->name = ucfirst(str_replace('_', ' ', $this->index));
}
/**
* @return string
* @return array
*/
public function getIndex(): string
{
return $this->index;
}
public function setIndex(string $index): IInputItem
{
$this->index = $index;
return $this;
}
/**
* @return string
*/
public function getName(): ?string
{
public function getName() {
return $this->name;
}
/**
* @return array
*/
public function getValue() {
return $this->value;
}
/**
* @return string
*/
public function getIndex() {
return $this->index;
}
/**
* Set input name
* @param string $name
* @return static
* @return static $this
*/
public function setName(string $name): IInputItem
{
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Set input value
* @param mixed $value
* @return static
* @param string $value
* @return static $this
*/
public function setValue($value): IInputItem
{
public function setValue($value) {
$this->value = $value;
return $this;
}
public function offsetExists($offset): bool
{
return isset($this->value[$offset]);
public function __toString() {
return (string)$this->getValue();
}
public function offsetGet($offset): ?self
{
if ($this->offsetExists($offset) === true) {
return $this->value[$offset];
}
return null;
}
public function offsetSet($offset, $value): void
{
$this->value[$offset] = $value;
}
public function offsetUnset($offset): void
{
unset($this->value[$offset]);
}
public function __toString(): string
{
$value = $this->getValue();
return (is_array($value) === true) ? json_encode($value) : $value;
}
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->getValue());
}
}
+44 -82
View File
@@ -1,41 +1,24 @@
<?php
namespace Pecee\Http\Middleware;
use Pecee\Http\Middleware\Exceptions\TokenMismatchException;
use Pecee\CsrfToken;
use Pecee\Exception\TokenMismatchException;
use Pecee\Http\Request;
use Pecee\Http\Security\CookieTokenProvider;
use Pecee\Http\Security\ITokenProvider;
class BaseCsrfVerifier implements IMiddleware
{
public const POST_KEY = 'csrf_token';
public const HEADER_KEY = 'X-CSRF-TOKEN';
class BaseCsrfVerifier implements IMiddleware {
const POST_KEY = 'csrf-token';
const HEADER_KEY = 'X-CSRF-TOKEN';
/**
* Urls to ignore. You can use * to exclude all sub-urls on a given path.
* For example: /admin/*
* @var array|null
*/
protected $except;
protected $csrfToken;
protected $token;
/**
* Urls to include. Can be used to include urls from a certain path.
* @var array|null
*/
protected $include;
public function __construct() {
$this->csrfToken = new CsrfToken();
/**
* @var ITokenProvider
*/
protected $tokenProvider;
/**
* BaseCsrfVerifier constructor.
*/
public function __construct()
{
$this->tokenProvider = new CookieTokenProvider();
// Generate or get the CSRF-Token from Cookie.
$this->token = (!$this->hasToken()) ? $this->generateToken() : $this->csrfToken->getToken();
}
/**
@@ -43,40 +26,22 @@ class BaseCsrfVerifier implements IMiddleware
* @param Request $request
* @return bool
*/
protected function skip(Request $request): bool
{
if ($this->except === null || count($this->except) === 0) {
protected function skip(Request $request) {
if($this->except === null || !is_array($this->except)) {
return false;
}
foreach($this->except as $url) {
$url = rtrim($url, '/');
if ($url[strlen($url) - 1] === '*') {
if($url[strlen($url)-1] === '*') {
$url = rtrim($url, '*');
$skip = $request->getUrl()->contains($url);
$skip = (stripos($request->getUri(), $url) === 0);
} else {
$skip = ($url === rtrim($request->getUrl()->getRelativeUrl(false), '/'));
$skip = ($url === rtrim($request->getUri(), '/'));
}
if ($skip === true) {
if(is_array($this->include) === true && count($this->include) > 0) {
foreach($this->include as $includeUrl) {
$includeUrl = rtrim($includeUrl, '/');
if ($includeUrl[strlen($includeUrl) - 1] === '*') {
$includeUrl = rtrim($includeUrl, '*');
$skip = !$request->getUrl()->contains($includeUrl);
break;
}
$skip = !($includeUrl === rtrim($request->getUrl()->getRelativeUrl(false), '/'));
}
}
if($skip === false) {
continue;
}
if($skip) {
return true;
}
}
@@ -84,44 +49,41 @@ class BaseCsrfVerifier implements IMiddleware
return false;
}
/**
* Handle request
*
* @param Request $request
* @throws TokenMismatchException
*/
public function handle(Request $request): void
{
if ($this->skip($request) === false && $request->isPostBack() === true) {
public function handle(Request $request) {
$token = $request->getInputHandler()->value(
static::POST_KEY,
$request->getHeader(static::HEADER_KEY),
Request::$requestTypesPost
);
if($request->getMethod() != 'get' && !$this->skip($request)) {
if ($this->tokenProvider->validate((string)$token) === false) {
throw new TokenMismatchException('Invalid CSRF-token.');
$token = (isset($_POST[static::POST_KEY])) ? $_POST[static::POST_KEY] : null;
// If the token is not posted, check headers for valid x-csrf-token
if($token === null) {
$token = $request->getHeader(static::HEADER_KEY);
}
if( !$this->csrfToken->validate($token) ) {
throw new TokenMismatchException('Invalid csrf-token.');
}
}
// Refresh existing token
$this->tokenProvider->refresh();
}
public function getTokenProvider(): ITokenProvider
{
return $this->tokenProvider;
public function generateToken() {
$token = $this->csrfToken->generateToken();
$this->csrfToken->setToken($token);
return $token;
}
/**
* Set token provider
* @param ITokenProvider $provider
*/
public function setTokenProvider(ITokenProvider $provider): void
{
$this->tokenProvider = $provider;
public function hasToken() {
if($this->token != null) {
return true;
}
return $this->csrfToken->hasToken();
}
public function getToken() {
return $this->token;
}
}
@@ -1,10 +0,0 @@
<?php
namespace Pecee\Http\Middleware\Exceptions;
use Exception;
class TokenMismatchException extends Exception
{
}
+2 -8
View File
@@ -1,14 +1,8 @@
<?php
namespace Pecee\Http\Middleware;
use Pecee\Http\Request;
interface IMiddleware
{
/**
* @param Request $request
*/
public function handle(Request $request): void;
interface IMiddleware {
public function handle(Request $request);
}
@@ -1,47 +0,0 @@
<?php
namespace Pecee\Http\Middleware;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Exceptions\HttpException;
abstract class IpRestrictAccess implements IMiddleware
{
protected $ipBlacklist = [];
protected $ipWhitelist = [];
protected function validate(string $ip): bool
{
// Accept ip that is in white-list
if(in_array($ip, $this->ipWhitelist, true) === true) {
return true;
}
foreach ($this->ipBlacklist as $blackIp) {
// Blocks range (8.8.*)
if ($blackIp[strlen($blackIp) - 1] === '*' && strpos($ip, trim($blackIp, '*')) === 0) {
return false;
}
// Blocks exact match
if ($blackIp === $ip) {
return false;
}
}
return true;
}
/**
* @param Request $request
* @throws HttpException
*/
public function handle(Request $request): void
{
if($this->validate((string)$request->getIp()) === false) {
throw new HttpException(sprintf('Restricted ip. Access to %s has been blocked', $request->getIp()), 403);
}
}
}
+94 -469
View File
@@ -1,181 +1,69 @@
<?php
namespace Pecee\Http;
use Pecee\Http\Exceptions\MalformedUrlException;
use Pecee\Http\Input\InputHandler;
use Pecee\Http\Middleware\BaseCsrfVerifier;
use Pecee\SimpleRouter\Route\ILoadableRoute;
use Pecee\SimpleRouter\Route\RouteUrl;
use Pecee\SimpleRouter\SimpleRouter;
use Pecee\Http\Input\Input;
class Request
{
public const REQUEST_TYPE_GET = 'get';
public const REQUEST_TYPE_POST = 'post';
public const REQUEST_TYPE_PUT = 'put';
public const REQUEST_TYPE_PATCH = 'patch';
public const REQUEST_TYPE_OPTIONS = 'options';
public const REQUEST_TYPE_DELETE = 'delete';
public const REQUEST_TYPE_HEAD = 'head';
class Request {
public const CONTENT_TYPE_JSON = 'application/json';
public const CONTENT_TYPE_FORM_DATA = 'multipart/form-data';
public const CONTENT_TYPE_X_FORM_ENCODED = 'application/x-www-form-urlencoded';
protected static $instance;
public const FORCE_METHOD_KEY = '_method';
protected $data;
/**
* All request-types
* @var string[]
* Return new instance
* @return static
*/
public static $requestTypes = [
self::REQUEST_TYPE_GET,
self::REQUEST_TYPE_POST,
self::REQUEST_TYPE_PUT,
self::REQUEST_TYPE_PATCH,
self::REQUEST_TYPE_OPTIONS,
self::REQUEST_TYPE_DELETE,
self::REQUEST_TYPE_HEAD,
];
/**
* Post request-types.
* @var string[]
*/
public static $requestTypesPost = [
self::REQUEST_TYPE_POST,
self::REQUEST_TYPE_PUT,
self::REQUEST_TYPE_PATCH,
self::REQUEST_TYPE_DELETE,
];
/**
* Additional data
*
* @var array
*/
private $data = [];
/**
* Server headers
* @var array
*/
protected $headers = [];
/**
* Request ContentType
* @var string
*/
protected $contentType;
/**
* Request host
* @var string
*/
protected $host;
/**
* Current request url
* @var Url
*/
protected $url;
/**
* Request method
* @var string
*/
protected $method;
/**
* Input handler
* @var InputHandler
*/
protected $inputHandler;
/**
* Defines if request has pending rewrite
* @var bool
*/
protected $hasPendingRewrite = false;
/**
* @var ILoadableRoute|null
*/
protected $rewriteRoute;
/**
* Rewrite url
* @var string|null
*/
protected $rewriteUrl;
/**
* @var array
*/
protected $loadedRoutes = [];
/**
* Request constructor.
* @throws MalformedUrlException
*/
public function __construct()
{
foreach ($_SERVER as $key => $value) {
$this->headers[strtolower($key)] = $value;
$this->headers[str_replace('_', '-', strtolower($key))] = $value;
public static function getInstance() {
if(self::$instance === null) {
self::$instance = new static();
}
return self::$instance;
}
$this->setHost($this->getHeader('http-host'));
public function __construct() {
$this->data = array();
$this->host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : array();
$this->uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : array();
$this->method = (isset($_POST['_method'])) ? strtolower($_POST['_method']) : (isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : array());
$this->headers = $this->getAllHeaders();
$this->input = new Input($this);
}
// Check if special IIS header exist, otherwise use default.
$url = $this->getHeader('unencoded-url');
if($url !== null){
$this->setUrl(new Url($url));
}else{
$this->setUrl(new Url(urldecode((string)$this->getHeader('request-uri'))));
protected function getAllHeaders() {
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) === 'HTTP_') {
$headers[strtolower(str_replace('_', '-', substr($name, 5)))] = $value;
}
}
$this->setContentType((string)$this->getHeader('content-type'));
$this->setMethod((string)($_POST[static::FORCE_METHOD_KEY] ?? $this->getHeader('request-method')));
$this->inputHandler = new InputHandler($this);
return $headers;
}
public function isSecure(): bool
{
return $this->getHeader('http-x-forwarded-proto') === 'https' || $this->getHeader('https') !== null || (int)$this->getHeader('server-port') === 443;
public function getIsSecure() {
if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
return true;
}
return isset($_SERVER['HTTPS']) ? true : (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] === 443);
}
/**
* @return Url
* @return string
*/
public function getUrl(): Url
{
return $this->url;
public function getUri() {
return $this->uri;
}
/**
* Copy url object
*
* @return Url
* @return string
*/
public function getUrlCopy(): Url
{
return clone $this->url;
}
/**
* @return string|null
*/
public function getHost(): ?string
{
public function getHost() {
return $this->host;
}
/**
* @return string|null
* @return string
*/
public function getMethod(): ?string
{
public function getMethod() {
return $this->method;
}
@@ -183,379 +71,116 @@ class Request
* Get http basic auth user
* @return string|null
*/
public function getUser(): ?string
{
return $this->getHeader('php-auth-user');
public function getUser() {
return (isset($_SERVER['PHP_AUTH_USER'])) ? $_SERVER['PHP_AUTH_USER']: null;
}
/**
* Get http basic auth password
* @return string|null
*/
public function getPassword(): ?string
{
return $this->getHeader('php-auth-pw');
public function getPassword() {
return (isset($_SERVER['PHP_AUTH_PW'])) ? $_SERVER['PHP_AUTH_PW']: null;
}
/**
* Get the csrf token
* @return string|null
*/
public function getCsrfToken(): ?string
{
return $this->getHeader(BaseCsrfVerifier::HEADER_KEY);
}
/**
* Get all headers
* Get headers
* @return array
*/
public function getHeaders(): array
{
public function getHeaders() {
return $this->headers;
}
/**
* Get id address
* If $safe is false, this function will detect Proxys. But the user can edit this header to whatever he wants!
* https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php#comment-25086804
* @param bool $safeMode When enabled, only safe non-spoofable headers will be returned. Note this can cause issues when using proxy.
* @return string|null
* @return string
*/
public function getIp(bool $safeMode = false): ?string
{
$headers = ['remote-addr'];
if($safeMode === false) {
$headers = array_merge($headers, [
'http-cf-connecting-ip',
'http-client-ip',
'http-x-forwarded-for',
]);
public function getIp() {
if(isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
return $this->getFirstHeader($headers);
}
/**
* Get remote address/ip
*
* @alias static::getIp
* @return string|null
*/
public function getRemoteAddr(): ?string
{
return $this->getIp();
return ((isset($_SERVER['HTTP_X_FORWARDED_FOR']) && strlen($_SERVER['HTTP_X_FORWARDED_FOR'])) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null);
}
/**
* Get referer
* @return string|null
* @return string
*/
public function getReferer(): ?string
{
return $this->getHeader('http-referer');
public function getReferer() {
return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
}
/**
* Get user agent
* @return string|null
* @return string
*/
public function getUserAgent(): ?string
{
return $this->getHeader('http-user-agent');
public function getUserAgent() {
return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
}
/**
* Get header value by name
*
* @param string $name Name of the header.
* @param string|mixed|null $defaultValue Value to be returned if header is not found.
* @param bool $tryParse When enabled the method will try to find the header from both from client (http) and server-side variants, if the header is not found.
*
* @param string $name
* @return string|null
*/
public function getHeader(string $name, $defaultValue = null, bool $tryParse = true): ?string
{
$name = strtolower($name);
$header = $this->headers[$name] ?? null;
if ($tryParse === true && $header === null) {
if (strpos($name, 'http-') === 0) {
// Trying to find client header variant which was not found, searching for header variant without http- prefix.
$header = $this->headers[str_replace('http-', '', $name)] ?? null;
} else {
// Trying to find server variant which was not found, searching for client variant with http- prefix.
$header = $this->headers['http-' . $name] ?? null;
}
}
return $header ?? $defaultValue;
}
/**
* Will try to find first header from list of headers.
*
* @param array $headers
* @param mixed|null $defaultValue
* @return mixed|null
*/
public function getFirstHeader(array $headers, $defaultValue = null)
{
foreach($headers as $header) {
$header = $this->getHeader($header);
if($header !== null) {
return $header;
}
}
return $defaultValue;
}
/**
* Get request content-type
* @return string|null
*/
public function getContentType(): ?string
{
return $this->contentType;
}
/**
* Set request content-type
* @param string $contentType
* @return $this
*/
protected function setContentType(string $contentType): self
{
if(strpos($contentType, ';') > 0) {
$this->contentType = strtolower(substr($contentType, 0, strpos($contentType, ';')));
} else {
$this->contentType = strtolower($contentType);
}
return $this;
public function getHeader($name) {
return (isset($this->headers[strtolower($name)])) ? $this->headers[strtolower($name)] : null;
}
/**
* Get input class
* @return InputHandler
* @return Input
*/
public function getInputHandler(): InputHandler
{
return $this->inputHandler;
public function getInput() {
return $this->input;
}
/**
* Is format accepted
*
* @param string $format
*
* @return bool
*/
public function isFormatAccepted(string $format): bool
{
return ($this->getHeader('http-accept') !== null && stripos($this->getHeader('http-accept'), $format) !== false);
public function isFormatAccepted($format) {
return (isset($_SERVER['HTTP_ACCEPT']) && stripos($_SERVER['HTTP_ACCEPT'], $format) > -1);
}
/**
* Returns true if the request is made through Ajax
*
* @return bool
*/
public function isAjax(): bool
{
return (strtolower((string)$this->getHeader('http-x-requested-with')) === 'xmlhttprequest');
}
/**
* Returns true when request-method is type that could contain data in the page body.
*
* @return bool
*/
public function isPostBack(): bool
{
return in_array($this->getMethod(), static::$requestTypesPost, true);
}
/**
* Get accept formats
* @return array
*/
public function getAcceptFormats(): array
{
return explode(',', $this->getHeader('http-accept'));
}
/**
* @param Url $url
*/
public function setUrl(Url $url): void
{
$this->url = $url;
if ($this->url->getHost() === null) {
$this->url->setHost((string)$this->getHost());
public function getAcceptFormats() {
if(isset($_SERVER['HTTP_ACCEPT'])) {
return explode(',', $_SERVER['HTTP_ACCEPT']);
}
return array();
}
if($this->isSecure() === true) {
$this->url->setScheme('https');
}
public function __set($name, $value = null) {
$this->data[$name] = $value;
}
public function __get($name) {
return isset($this->data[$name]) ? $this->data[$name] : null;
}
/**
* @param string|null $host
* Get the currently loaded route.
* @return \Pecee\SimpleRouter\RouterEntry
*/
public function setHost(?string $host): void
{
public function getLoadedRoute() {
return $this->loadedRoute;
}
/**
* @param mixed $uri
*/
public function setUri($uri) {
$this->uri = $uri;
}
/**
* @param mixed $host
*/
public function setHost($host) {
$this->host = $host;
}
/**
* @param string $method
* @param mixed $method
*/
public function setMethod(string $method): void
{
$this->method = strtolower($method);
}
/**
* Set rewrite route
*
* @param ILoadableRoute $route
* @return static
*/
public function setRewriteRoute(ILoadableRoute $route): self
{
$this->hasPendingRewrite = true;
$this->rewriteRoute = SimpleRouter::addDefaultNamespace($route);
return $this;
}
/**
* Get rewrite route
*
* @return ILoadableRoute|null
*/
public function getRewriteRoute(): ?ILoadableRoute
{
return $this->rewriteRoute;
}
/**
* Get rewrite url
*
* @return string|null
*/
public function getRewriteUrl(): ?string
{
return $this->rewriteUrl;
}
/**
* Set rewrite url
*
* @param string $rewriteUrl
* @return static
*/
public function setRewriteUrl(string $rewriteUrl): self
{
$this->hasPendingRewrite = true;
$this->rewriteUrl = rtrim($rewriteUrl, '/') . '/';
return $this;
}
/**
* Set rewrite callback
* @param string|\Closure $callback
* @return static
*/
public function setRewriteCallback($callback): self
{
$this->hasPendingRewrite = true;
return $this->setRewriteRoute(new RouteUrl($this->getUrl()->getPath(), $callback));
}
/**
* Get loaded route
* @return ILoadableRoute|null
*/
public function getLoadedRoute(): ?ILoadableRoute
{
return (count($this->loadedRoutes) > 0) ? end($this->loadedRoutes) : null;
}
/**
* Get all loaded routes
*
* @return array
*/
public function getLoadedRoutes(): array
{
return $this->loadedRoutes;
}
/**
* Set loaded routes
*
* @param array $routes
* @return static
*/
public function setLoadedRoutes(array $routes): self
{
$this->loadedRoutes = $routes;
return $this;
}
/**
* Added loaded route
*
* @param ILoadableRoute $route
* @return static
*/
public function addLoadedRoute(ILoadableRoute $route): self
{
$this->loadedRoutes[] = $route;
return $this;
}
/**
* Returns true if the request contains a rewrite
*
* @return bool
*/
public function hasPendingRewrite(): bool
{
return $this->hasPendingRewrite;
}
/**
* Defines if the current request contains a rewrite.
*
* @param bool $boolean
* @return Request
*/
public function setHasPendingRewrite(bool $boolean): self
{
$this->hasPendingRewrite = $boolean;
return $this;
}
public function __isset($name): bool
{
return array_key_exists($name, $this->data) === true;
}
public function __set($name, $value = null)
{
$this->data[$name] = $value;
}
public function __get($name)
{
return $this->data[$name] ?? null;
public function setMethod($method) {
$this->method = $method;
}
}
+35 -57
View File
@@ -2,28 +2,16 @@
namespace Pecee\Http;
use JsonSerializable;
use Pecee\Exceptions\InvalidArgumentException;
class Response
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
class Response {
/**
* Set the http status code
*
* @param int $code
* @return static
* @return self $this
*/
public function httpCode(int $code): self
{
public function httpCode($code) {
http_response_code($code);
return $this;
}
@@ -31,54 +19,50 @@ class Response
* Redirect the response
*
* @param string $url
* @param ?int $httpCode
* @param int $httpCode
*/
public function redirect(string $url, ?int $httpCode = null): void
{
if ($httpCode !== null) {
public function redirect($url, $httpCode = null) {
if($httpCode !== null) {
$this->httpCode($httpCode);
}
$this->header('location: ' . $url);
exit(0);
die();
}
public function refresh(): void
{
$this->redirect($this->request->getUrl()->getOriginalUrl());
public function refresh() {
$this->redirect(Request::getInstance()->getUri());
}
/**
* Add http authorisation
* @param string $name
* @return static
* @return self $this
*/
public function auth(string $name = ''): self
{
public function auth($name = '') {
$this->headers([
'WWW-Authenticate: Basic realm="' . $name . '"',
'HTTP/1.0 401 Unauthorized',
'HTTP/1.0 401 Unauthorized'
]);
return $this;
}
public function cache(string $eTag, int $lastModifiedTime = 2592000): self
{
public function cache($eTag, $lastModified = 2592000) {
$this->headers([
'Cache-Control: public',
sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $lastModifiedTime)),
sprintf('Etag: %s', $eTag),
'Last-Modified: ' . gmdate("D, d M Y H:i:s", $lastModified) . ' GMT',
'Etag: ' . $eTag
]);
$httpModified = $this->request->getHeader('http-if-modified-since');
$httpIfNoneMatch = $this->request->getHeader('http-if-none-match');
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $lastModified ||
isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $eTag) {
if (($httpIfNoneMatch !== null && $httpIfNoneMatch === $eTag) || ($httpModified !== null && strtotime($httpModified) === $lastModifiedTime)) {
$this->headers([
'HTTP/1.1 304 Not Modified'
]);
$this->header('HTTP/1.1 304 Not Modified');
exit(0);
exit();
}
return $this;
@@ -86,45 +70,39 @@ class Response
/**
* Json encode
* @param array|JsonSerializable $value
* @param int $options JSON options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT, JSON_PRESERVE_ZERO_FRACTION, JSON_UNESCAPED_UNICODE, JSON_PARTIAL_OUTPUT_ON_ERROR.
* @param int $dept JSON debt.
* @throws InvalidArgumentException
* @param array|\JsonSerializable $value
* @throws \InvalidArgumentException;
*/
public function json($value, int $options = 0, int $dept = 512): void
{
if (($value instanceof JsonSerializable) === false && is_array($value) === false) {
throw new InvalidArgumentException('Invalid type for parameter "value". Must be of type array or object implementing the \JsonSerializable interface.');
public function json($value) {
if(($value instanceof \JsonSerializable) === false && is_array($value) === false) {
throw new \InvalidArgumentException('Invalid type for parameter "value". Must be of type array or object implementing the \JsonSerializable interface.');
}
$this->header('Content-Type: application/json; charset=utf-8');
echo json_encode($value, $options, $dept);
$this->header('Content-type: application/json');
echo json_encode($value);
exit(0);
}
/**
* Add header to response
* @param string $value
* @return static
* @return self $this
*/
public function header(string $value): self
{
public function header($value) {
header($value);
return $this;
}
/**
* Add multiple headers to response
* @param array $headers
* @return static
* @return self $this
*/
public function headers(array $headers): self
{
foreach ($headers as $header) {
$this->header($header);
public function headers(array $headers) {
foreach($headers as $header) {
header($header);
}
return $this;
}
@@ -1,124 +0,0 @@
<?php
namespace Pecee\Http\Security;
use Exception;
use Pecee\Http\Security\Exceptions\SecurityException;
class CookieTokenProvider implements ITokenProvider
{
public const CSRF_KEY = 'CSRF-TOKEN';
/**
* @var string
*/
protected $token;
/**
* @var int
*/
protected $cookieTimeoutMinutes = 120;
/**
* CookieTokenProvider constructor.
* @throws SecurityException
*/
public function __construct()
{
$this->token = ($this->hasToken() === true) ? $_COOKIE[static::CSRF_KEY] : null;
if ($this->token === null) {
$this->token = $this->generateToken();
}
}
/**
* Generate random identifier for CSRF token
*
* @return string
* @throws SecurityException
*/
public function generateToken(): string
{
try {
return bin2hex(random_bytes(32));
} catch (Exception $e) {
throw new SecurityException($e->getMessage(), (int)$e->getCode(), $e->getPrevious());
}
}
/**
* Validate valid CSRF token
*
* @param string $token
* @return bool
*/
public function validate(string $token): bool
{
if ($this->getToken() !== null) {
return hash_equals($token, $this->getToken());
}
return false;
}
/**
* Set csrf token cookie
* Overwrite this method to save the token to another storage like session etc.
*
* @param string $token
*/
public function setToken(string $token): void
{
$this->token = $token;
setcookie(static::CSRF_KEY, $token, time() + (60 * $this->cookieTimeoutMinutes), '/', ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly'));
}
/**
* Get csrf token
* @param string|null $defaultValue
* @return string|null
*/
public function getToken(?string $defaultValue = null): ?string
{
return $this->token ?? $defaultValue;
}
/**
* Refresh existing token
*/
public function refresh(): void
{
if ($this->token !== null) {
$this->setToken($this->token);
}
}
/**
* Returns whether the csrf token has been defined
* @return bool
*/
public function hasToken(): bool
{
return isset($_COOKIE[static::CSRF_KEY]);
}
/**
* Get timeout for cookie in minutes
* @return int
*/
public function getCookieTimeoutMinutes(): int
{
return $this->cookieTimeoutMinutes;
}
/**
* Set cookie timeout in minutes
* @param int $minutes
*/
public function setCookieTimeoutMinutes(int $minutes): void
{
$this->cookieTimeoutMinutes = $minutes;
}
}
@@ -1,10 +0,0 @@
<?php
namespace Pecee\Http\Security\Exceptions;
use Exception;
class SecurityException extends Exception
{
}
@@ -1,29 +0,0 @@
<?php
namespace Pecee\Http\Security;
interface ITokenProvider
{
/**
* Refresh existing token
*/
public function refresh(): void;
/**
* Validate valid CSRF token
*
* @param string $token
* @return bool
*/
public function validate(string $token): bool;
/**
* Get token token
*
* @param string|null $defaultValue
* @return string|null
*/
public function getToken(?string $defaultValue = null): ?string;
}
-517
View File
@@ -1,517 +0,0 @@
<?php
namespace Pecee\Http;
use JsonSerializable;
use Pecee\Http\Exceptions\MalformedUrlException;
class Url implements JsonSerializable
{
/**
* @var string|null
*/
private $originalUrl;
/**
* @var string|null
*/
private $scheme;
/**
* @var string|null
*/
private $host;
/**
* @var int|null
*/
private $port;
/**
* @var string|null
*/
private $username;
/**
* @var string|null
*/
private $password;
/**
* @var string|null
*/
private $path;
/**
* @var array
*/
private $params = [];
/**
* @var string|null
*/
private $fragment;
/**
* Url constructor.
*
* @param ?string $url
* @throws MalformedUrlException
*/
public function __construct(?string $url)
{
$this->originalUrl = $url;
if ($url !== null && $url !== '/') {
$data = $this->parseUrl($url);
$this->scheme = $data['scheme'] ?? null;
$this->host = $data['host'] ?? null;
$this->port = $data['port'] ?? null;
$this->username = $data['user'] ?? null;
$this->password = $data['pass'] ?? null;
if (isset($data['path']) === true) {
$this->setPath($data['path']);
}
$this->fragment = $data['fragment'] ?? null;
if (isset($data['query']) === true) {
$this->setQueryString($data['query']);
}
}
}
/**
* Check if url is using a secure protocol like https
*
* @return bool
*/
public function isSecure(): bool
{
return (strtolower($this->getScheme()) === 'https');
}
/**
* Checks if url is relative
*
* @return bool
*/
public function isRelative(): bool
{
return ($this->getHost() === null);
}
/**
* Get url scheme
*
* @return string|null
*/
public function getScheme(): ?string
{
return $this->scheme;
}
/**
* Set the scheme of the url
*
* @param string $scheme
* @return static
*/
public function setScheme(string $scheme): self
{
$this->scheme = $scheme;
return $this;
}
/**
* Get url host
*
* @return string|null
*/
public function getHost(): ?string
{
return $this->host;
}
/**
* Set the host of the url
*
* @param string $host
* @return static
*/
public function setHost(string $host): self
{
$this->host = $host;
return $this;
}
/**
* Get url port
*
* @return int|null
*/
public function getPort(): ?int
{
return ($this->port !== null) ? (int)$this->port : null;
}
/**
* Set the port of the url
*
* @param int $port
* @return static
*/
public function setPort(int $port): self
{
$this->port = $port;
return $this;
}
/**
* Parse username from url
*
* @return string|null
*/
public function getUsername(): ?string
{
return $this->username;
}
/**
* Set the username of the url
*
* @param string $username
* @return static
*/
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* Parse password from url
* @return string|null
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* Set the url password
*
* @param string $password
* @return static
*/
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* Get path from url
* @return string
*/
public function getPath(): ?string
{
return $this->path ?? '/';
}
/**
* Set the url path
*
* @param string $path
* @return static
*/
public function setPath(string $path): self
{
$this->path = rtrim($path, '/') . '/';
return $this;
}
/**
* Get query-string from url
*
* @return array
*/
public function getParams(): array
{
return $this->params;
}
/**
* Merge parameters array
*
* @param array $params
* @return static
*/
public function mergeParams(array $params): self
{
return $this->setParams(array_merge($this->getParams(), $params));
}
/**
* Set the url params
*
* @param array $params
* @return static
*/
public function setParams(array $params): self
{
$this->params = $params;
return $this;
}
/**
* Set raw query-string parameters as string
*
* @param string $queryString
* @return static
*/
public function setQueryString(string $queryString): self
{
$params = [];
parse_str($queryString, $params);
if(count($params) > 0) {
return $this->setParams($params);
}
return $this;
}
/**
* Get query-string params as string
*
* @return string
*/
public function getQueryString(): string
{
return static::arrayToParams($this->getParams());
}
/**
* Get fragment from url (everything after #)
*
* @return string|null
*/
public function getFragment(): ?string
{
return $this->fragment;
}
/**
* Set url fragment
*
* @param string $fragment
* @return static
*/
public function setFragment(string $fragment): self
{
$this->fragment = $fragment;
return $this;
}
/**
* @return string
*/
public function getOriginalUrl(): string
{
return $this->originalUrl;
}
/**
* Get position of value.
* Returns -1 on failure.
*
* @param string $value
* @return int
*/
public function indexOf(string $value): int
{
$index = stripos($this->getOriginalUrl(), $value);
return ($index === false) ? -1 : $index;
}
/**
* Check if url contains value.
*
* @param string $value
* @return bool
*/
public function contains(string $value): bool
{
return (stripos($this->getOriginalUrl(), $value) !== false);
}
/**
* Check if url contains parameter/query string.
*
* @param string $name
* @return bool
*/
public function hasParam(string $name): bool
{
return array_key_exists($name, $this->getParams());
}
/**
* Removes multiple parameters from the query-string
*
* @param array ...$names
* @return static
*/
public function removeParams(...$names): self
{
$params = array_diff_key($this->getParams(), array_flip(...$names));
$this->setParams($params);
return $this;
}
/**
* Removes parameter from the query-string
*
* @param string $name
* @return static
*/
public function removeParam(string $name): self
{
$params = $this->getParams();
unset($params[$name]);
$this->setParams($params);
return $this;
}
/**
* Get parameter by name.
* Returns parameter value or default value.
*
* @param string $name
* @param string|null $defaultValue
* @return string|null
*/
public function getParam(string $name, ?string $defaultValue = null): ?string
{
return (isset($this->getParams()[$name]) === true) ? $this->getParams()[$name] : $defaultValue;
}
/**
* UTF-8 aware parse_url() replacement.
* @param string $url
* @param int $component
* @return array
* @throws MalformedUrlException
*/
public function parseUrl(string $url, int $component = -1): array
{
$encodedUrl = preg_replace_callback(
'/[^:\/@?&=#]+/u',
static function ($matches): string {
return urlencode($matches[0]);
},
$url
);
$parts = parse_url($encodedUrl, $component);
if ($parts === false) {
throw new MalformedUrlException(sprintf('Failed to parse url: "%s"', $url));
}
return array_map('urldecode', $parts);
}
/**
* Convert array to query-string params
*
* @param array $getParams
* @param bool $includeEmpty
* @return string
*/
public static function arrayToParams(array $getParams = [], bool $includeEmpty = true): string
{
if (count($getParams) !== 0) {
if ($includeEmpty === false) {
$getParams = array_filter($getParams, static function ($item): bool {
return (trim($item) !== '');
});
}
return http_build_query($getParams);
}
return '';
}
/**
* Returns the relative url
*
* @param bool $includeParams
* @return string
*/
public function getRelativeUrl(bool $includeParams = true): string
{
$path = $this->path ?? '/';
if($includeParams === false) {
return $path;
}
$query = $this->getQueryString() !== '' ? '?' . $this->getQueryString() : '';
$fragment = $this->fragment !== null ? '#' . $this->fragment : '';
return $path . $query . $fragment;
}
/**
* Returns the absolute url
*
* @param bool $includeParams
* @return string
*/
public function getAbsoluteUrl(bool $includeParams = true): string
{
$scheme = $this->scheme !== null ? $this->scheme . '://' : '';
$host = $this->host ?? '';
$port = $this->port !== null ? ':' . $this->port : '';
$user = $this->username ?? '';
$pass = $this->password !== null ? ':' . $this->password : '';
$pass = ($user !== '' || $pass !== '') ? $pass . '@' : '';
return $scheme . $user . $pass . $host . $port . $this->getRelativeUrl($includeParams);
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return string data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize(): string
{
return $this->getRelativeUrl();
}
public function __toString(): string
{
return $this->getRelativeUrl();
}
}
@@ -1,49 +0,0 @@
<?php
namespace Pecee\SimpleRouter\ClassLoader;
use Pecee\SimpleRouter\Exceptions\ClassNotFoundHttpException;
class ClassLoader implements IClassLoader
{
/**
* Load class
*
* @param string $class
* @return object
* @throws ClassNotFoundHttpException
*/
public function loadClass(string $class)
{
if (class_exists($class) === false) {
throw new ClassNotFoundHttpException($class, null, sprintf('Class "%s" does not exist', $class), 404, null);
}
return new $class();
}
/**
* Called when loading class method
* @param object $class
* @param string $method
* @param array $parameters
* @return object
*/
public function loadClassMethod($class, string $method, array $parameters)
{
return call_user_func_array([$class, $method], array_values($parameters));
}
/**
* Load closure
*
* @param Callable $closure
* @param array $parameters
* @return mixed
*/
public function loadClosure(Callable $closure, array $parameters)
{
return call_user_func_array($closure, array_values($parameters));
}
}
@@ -1,33 +0,0 @@
<?php
namespace Pecee\SimpleRouter\ClassLoader;
interface IClassLoader
{
/**
* Called when loading class
* @param string $class
* @return object
*/
public function loadClass(string $class);
/**
* Called when loading class method
* @param object $class
* @param string $method
* @param array $parameters
* @return object
*/
public function loadClassMethod($class, string $method, array $parameters);
/**
* Called when loading method
*
* @param callable $closure
* @param array $parameters
* @return mixed
*/
public function loadClosure(Callable $closure, array $parameters);
}
@@ -1,112 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Event;
use InvalidArgumentException;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Router;
class EventArgument implements IEventArgument
{
/**
* Event name
* @var string
*/
protected $eventName;
/**
* @var Router
*/
protected $router;
/**
* @var array
*/
protected $arguments = [];
public function __construct(string $eventName, Router $router, array $arguments = [])
{
$this->eventName = $eventName;
$this->router = $router;
$this->arguments = $arguments;
}
/**
* Get event name
*
* @return string
*/
public function getEventName(): string
{
return $this->eventName;
}
/**
* Set the event name
*
* @param string $name
*/
public function setEventName(string $name): void
{
$this->eventName = $name;
}
/**
* Get the router instance
*
* @return Router
*/
public function getRouter(): Router
{
return $this->router;
}
/**
* Get the request instance
*
* @return Request
*/
public function getRequest(): Request
{
return $this->getRouter()->getRequest();
}
/**
* @param string $name
* @return mixed
*/
public function __get(string $name)
{
return $this->arguments[$name] ?? null;
}
/**
* @param string $name
* @return bool
*/
public function __isset(string $name): bool
{
return array_key_exists($name, $this->arguments);
}
/**
* @param string $name
* @param mixed $value
* @throws InvalidArgumentException
*/
public function __set(string $name, $value): void
{
throw new InvalidArgumentException('Not supported');
}
/**
* Get arguments
*
* @return array
*/
public function getArguments(): array
{
return $this->arguments;
}
}
@@ -1,46 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Event;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Router;
interface IEventArgument
{
/**
* Get event name
*
* @return string
*/
public function getEventName(): string;
/**
* Set event name
*
* @param string $name
*/
public function setEventName(string $name): void;
/**
* Get router instance
*
* @return Router
*/
public function getRouter(): Router;
/**
* Get request instance
*
* @return Request
*/
public function getRequest(): Request;
/**
* Get all event arguments
*
* @return array
*/
public function getArguments(): array;
}
@@ -1,45 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Exceptions;
use Throwable;
class ClassNotFoundHttpException extends NotFoundHttpException
{
/**
* @var string
*/
protected $class;
/**
* @var string|null
*/
protected $method;
public function __construct(string $class, ?string $method = null, string $message = "", int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->class = $class;
$this->method = $method;
}
/**
* Get class name
* @return string
*/
public function getClass(): string
{
return $this->class;
}
/**
* Get method
* @return string|null
*/
public function getMethod(): ?string
{
return $this->method;
}
}
@@ -1,10 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Exceptions;
use Exception;
class HttpException extends Exception
{
}
@@ -1,8 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Exceptions;
class NotFoundHttpException extends HttpException
{
}
@@ -1,42 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Handlers;
use Closure;
use Exception;
use Pecee\Http\Request;
/**
* Class CallbackExceptionHandler
*
* Class is used to create callbacks which are fired when an exception is reached.
* This allows for easy handling 404-exception etc. without creating an custom ExceptionHandler.
*
* @package \Pecee\SimpleRouter\Handlers
*/
class CallbackExceptionHandler implements IExceptionHandler
{
/**
* @var Closure
*/
protected $callback;
public function __construct(Closure $callback)
{
$this->callback = $callback;
}
/**
* @param Request $request
* @param Exception $error
*/
public function handleError(Request $request, Exception $error): void
{
/* Fire exceptions */
call_user_func($this->callback,
$request,
$error
);
}
}
@@ -1,63 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Handlers;
use Closure;
use Pecee\SimpleRouter\Event\EventArgument;
use Pecee\SimpleRouter\Router;
class DebugEventHandler implements IEventHandler
{
/**
* Debug callback
* @var Closure
*/
protected $callback;
public function __construct()
{
$this->callback = static function (EventArgument $argument): void {
// todo: log in database
};
}
/**
* Get events.
*
* @param string|null $name Filter events by name.
* @return array
*/
public function getEvents(?string $name): array
{
return [
$name => [
$this->callback,
],
];
}
/**
* Fires any events registered with given event-name
*
* @param Router $router Router instance
* @param string $name Event name
* @param array $eventArgs Event arguments
*/
public function fireEvents(Router $router, string $name, array $eventArgs = []): void
{
$callback = $this->callback;
$callback(new EventArgument($name, $router, $eventArgs));
}
/**
* Set debug callback
*
* @param Closure $event
*/
public function setCallback(Closure $event): void
{
$this->callback = $event;
}
}
@@ -1,185 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Handlers;
use Closure;
use Pecee\SimpleRouter\Event\EventArgument;
use Pecee\SimpleRouter\Router;
class EventHandler implements IEventHandler
{
/**
* Fires when a event is triggered.
*/
public const EVENT_ALL = '*';
/**
* Fires when router is initializing and before routes are loaded.
*/
public const EVENT_INIT = 'onInit';
/**
* Fires when all routes has been loaded and rendered, just before the output is returned.
*/
public const EVENT_LOAD = 'onLoad';
/**
* Fires when route is added to the router
*/
public const EVENT_ADD_ROUTE = 'onAddRoute';
/**
* Fires when a url-rewrite is and just before the routes are re-initialized.
*/
public const EVENT_REWRITE = 'onRewrite';
/**
* Fires when the router is booting.
* This happens just before boot-managers are rendered and before any routes has been loaded.
*/
public const EVENT_BOOT = 'onBoot';
/**
* Fires before a boot-manager is rendered.
*/
public const EVENT_RENDER_BOOTMANAGER = 'onRenderBootManager';
/**
* Fires when the router is about to load all routes.
*/
public const EVENT_LOAD_ROUTES = 'onLoadRoutes';
/**
* Fires whenever the `findRoute` method is called within the `Router`.
* This usually happens when the router tries to find routes that
* contains a certain url, usually after the EventHandler::EVENT_GET_URL event.
*/
public const EVENT_FIND_ROUTE = 'onFindRoute';
/**
* Fires whenever the `Router::getUrl` method or `url`-helper function
* is called and the router tries to find the route.
*/
public const EVENT_GET_URL = 'onGetUrl';
/**
* Fires when a route is matched and valid (correct request-type etc).
* and before the route is rendered.
*/
public const EVENT_MATCH_ROUTE = 'onMatchRoute';
/**
* Fires before a route is rendered.
*/
public const EVENT_RENDER_ROUTE = 'onRenderRoute';
/**
* Fires when the router is loading exception-handlers.
*/
public const EVENT_LOAD_EXCEPTIONS = 'onLoadExceptions';
/**
* Fires before the router is rendering a exception-handler.
*/
public const EVENT_RENDER_EXCEPTION = 'onRenderException';
/**
* Fires before a middleware is rendered.
*/
public const EVENT_RENDER_MIDDLEWARES = 'onRenderMiddlewares';
/**
* Fires before the CSRF-verifier is rendered.
*/
public const EVENT_RENDER_CSRF = 'onRenderCsrfVerifier';
/**
* All available events
* @var array
*/
public static $events = [
self::EVENT_ALL,
self::EVENT_INIT,
self::EVENT_LOAD,
self::EVENT_ADD_ROUTE,
self::EVENT_REWRITE,
self::EVENT_BOOT,
self::EVENT_RENDER_BOOTMANAGER,
self::EVENT_LOAD_ROUTES,
self::EVENT_FIND_ROUTE,
self::EVENT_GET_URL,
self::EVENT_MATCH_ROUTE,
self::EVENT_RENDER_ROUTE,
self::EVENT_LOAD_EXCEPTIONS,
self::EVENT_RENDER_EXCEPTION,
self::EVENT_RENDER_MIDDLEWARES,
self::EVENT_RENDER_CSRF,
];
/**
* List of all registered events
* @var array
*/
private $registeredEvents = [];
/**
* Register new event
*
* @param string $name
* @param Closure $callback
* @return static
*/
public function register(string $name, Closure $callback): IEventHandler
{
if (isset($this->registeredEvents[$name]) === true) {
$this->registeredEvents[$name][] = $callback;
} else {
$this->registeredEvents[$name] = [$callback];
}
return $this;
}
/**
* Get events.
*
* @param string|null $name Filter events by name.
* @param array|string ...$names Add multiple names...
* @return array
*/
public function getEvents(?string $name, ...$names): array
{
if ($name === null) {
return $this->registeredEvents;
}
$names[] = $name;
$events = [];
foreach ($names as $eventName) {
if (isset($this->registeredEvents[$eventName]) === true) {
$events += $this->registeredEvents[$eventName];
}
}
return $events;
}
/**
* Fires any events registered with given event-name
*
* @param Router $router Router instance
* @param string $name Event name
* @param array $eventArgs Event arguments
*/
public function fireEvents(Router $router, string $name, array $eventArgs = []): void
{
$events = $this->getEvents(static::EVENT_ALL, $name);
/* @var $event Closure */
foreach ($events as $event) {
$event(new EventArgument($name, $router, $eventArgs));
}
}
}
@@ -1,27 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Handlers;
use Pecee\SimpleRouter\Router;
interface IEventHandler
{
/**
* Get events.
*
* @param string|null $name Filter events by name.
* @return array
*/
public function getEvents(?string $name): array;
/**
* Fires any events registered with given event-name
*
* @param Router $router Router instance
* @param string $name Event name
* @param array $eventArgs Event arguments
*/
public function fireEvents(Router $router, string $name, array $eventArgs = []): void;
}
@@ -1,16 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Handlers;
use Exception;
use Pecee\Http\Request;
interface IExceptionHandler
{
/**
* @param Request $request
* @param Exception $error
*/
public function handleError(Request $request, Exception $error): void;
}
@@ -1,16 +0,0 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Http\Request;
interface IRouterBootManager
{
/**
* Called when router loads it's routes
*
* @param Router $router
* @param Request $request
*/
public function boot(Router $router, Request $request): void;
}
@@ -1,22 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
interface IControllerRoute extends ILoadableRoute
{
/**
* Get controller class-name
*
* @return string
*/
public function getController(): string;
/**
* Set controller class-name
*
* @param string $controller
* @return static
*/
public function setController(string $controller): self;
}
@@ -1,93 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Handlers\IExceptionHandler;
interface IGroupRoute extends IRoute
{
/**
* Method called to check if a domain matches
*
* @param Request $request
* @return bool
*/
public function matchDomain(Request $request): bool;
/**
* Add exception handler
*
* @param IExceptionHandler|string $handler
* @return static
*/
public function addExceptionHandler($handler): self;
/**
* Set exception-handlers for group
*
* @param array $handlers
* @return static
*/
public function setExceptionHandlers(array $handlers): self;
/**
* Returns true if group should overwrite existing exception-handlers.
*
* @return bool
*/
public function getMergeExceptionHandlers(): bool;
/**
* When enabled group will overwrite any existing exception-handlers.
*
* @param bool $merge
* @return static
*/
public function setMergeExceptionHandlers(bool $merge): self;
/**
* Get exception-handlers for group
*
* @return array
*/
public function getExceptionHandlers(): array;
/**
* Get domains for domain.
*
* @return array
*/
public function getDomains(): array;
/**
* Set allowed domains for group.
*
* @param array $domains
* @return static
*/
public function setDomains(array $domains): self;
/**
* Prepends prefix while ensuring that the url has the correct formatting.
*
* @param string $url
* @return static
*/
public function prependPrefix(string $url): self;
/**
* Set prefix that child-routes will inherit.
*
* @param string $prefix
* @return static
*/
public function setPrefix(string $prefix): self;
/**
* Get prefix.
*
* @return string|null
*/
public function getPrefix(): ?string;
}
@@ -1,87 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Router;
interface ILoadableRoute extends IRoute
{
/**
* Find url that matches method, parameters or name.
* Used when calling the url() helper.
*
* @param string|null $method
* @param array|string|null $parameters
* @param string|null $name
* @return string
*/
public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string;
/**
* Loads and renders middleware-classes
*
* @param Request $request
* @param Router $router
*/
public function loadMiddleware(Request $request, Router $router): void;
/**
* Get url
* @return string
*/
public function getUrl(): string;
/**
* Set url
* @param string $url
* @return static
*/
public function setUrl(string $url): self;
/**
* Prepends url while ensuring that the url has the correct formatting.
* @param string $url
* @return ILoadableRoute
*/
public function prependUrl(string $url): self;
/**
* Returns the provided name for the router.
*
* @return string|null
*/
public function getName(): ?string;
/**
* Check if route has given name.
*
* @param string $name
* @return bool
*/
public function hasName(string $name): bool;
/**
* Sets the router name, which makes it easier to obtain the url or router at a later point.
*
* @param string $name
* @return static
*/
public function setName(string $name): self;
/**
* Get regular expression match used for matching route (if defined).
*
* @return string
*/
public function getMatch(): ?string;
/**
* Add regular expression match for the entire route.
*
* @param string $regex
* @return static
*/
public function setMatch(string $regex): self;
}
@@ -1,8 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
interface IPartialGroupRoute
{
}
-223
View File
@@ -1,223 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Router;
interface IRoute
{
/**
* Method called to check if a domain matches
*
* @param string $url
* @param Request $request
* @return bool
*/
public function matchRoute(string $url, Request $request): bool;
/**
* Called when route is matched.
* Returns class to be rendered.
*
* @param Request $request
* @param Router $router
* @return string
* @throws \Pecee\SimpleRouter\Exceptions\NotFoundHttpException
*/
public function renderRoute(Request $request, Router $router): ?string;
/**
* Returns callback name/identifier for the current route based on the callback.
* Useful if you need to get a unique identifier for the loaded route, for instance
* when using translations etc.
*
* @return string
*/
public function getIdentifier(): string;
/**
* Set allowed request methods
*
* @param array $methods
* @return static
*/
public function setRequestMethods(array $methods): self;
/**
* Get allowed request methods
*
* @return array
*/
public function getRequestMethods(): array;
/**
* @return IRoute|null
*/
public function getParent(): ?IRoute;
/**
* Get the group for the route.
*
* @return IGroupRoute|null
*/
public function getGroup(): ?IGroupRoute;
/**
* Set group
*
* @param IGroupRoute $group
* @return static
*/
public function setGroup(IGroupRoute $group): self;
/**
* Set parent route
*
* @param IRoute $parent
* @return static
*/
public function setParent(IRoute $parent): self;
/**
* Set callback
*
* @param string|array|\Closure $callback
* @return static
*/
public function setCallback($callback): self;
/**
* @return string|callable
*/
public function getCallback();
/**
* Return active method
*
* @return string|null
*/
public function getMethod(): ?string;
/**
* Set active method
*
* @param string $method
* @return static
*/
public function setMethod(string $method): self;
/**
* Get class
*
* @return string|null
*/
public function getClass(): ?string;
/**
* @param string $namespace
* @return static
*/
public function setNamespace(string $namespace): self;
/**
* @return string|null
*/
public function getNamespace(): ?string;
/**
* @param string $namespace
* @return static
*/
public function setDefaultNamespace(string $namespace): IRoute;
/**
* Get default namespace
* @return string|null
*/
public function getDefaultNamespace(): ?string;
/**
* Get parameter names.
*
* @return array
*/
public function getWhere(): array;
/**
* Set parameter names.
*
* @param array $options
* @return static
*/
public function setWhere(array $options): self;
/**
* Get parameters
*
* @return array
*/
public function getParameters(): array;
/**
* Get parameters
*
* @param array $parameters
* @return static
*/
public function setParameters(array $parameters): self;
/**
* Merge with information from another route.
*
* @param array $settings
* @param bool $merge
* @return static
*/
public function setSettings(array $settings, bool $merge = false): self;
/**
* Export route settings to array so they can be merged with another route.
*
* @return array
*/
public function toArray(): array;
/**
* Get middlewares array
*
* @return array
*/
public function getMiddlewares(): array;
/**
* Set middleware class-name
*
* @param string $middleware
* @return static
*/
public function addMiddleware(string $middleware): self;
/**
* Set middlewares array
*
* @param array $middlewares
* @return static
*/
public function setMiddlewares(array $middlewares): self;
/**
* If enabled parameters containing null-value will not be passed along to the callback.
*
* @param bool $enabled
* @return static $this
*/
public function setFilterEmptyParams(bool $enabled): self;
/**
* Status if filtering of empty params is enabled or disabled
* @return bool
*/
public function getFilterEmptyParams(): bool;
}
@@ -1,277 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Exceptions\HttpException;
use Pecee\SimpleRouter\Router;
abstract class LoadableRoute extends Route implements ILoadableRoute
{
/**
* @var string
*/
protected $url;
/**
* @var string
*/
protected $name;
/**
* @var string|null
*/
protected $regex;
/**
* Loads and renders middlewares-classes
*
* @param Request $request
* @param Router $router
* @throws HttpException
*/
public function loadMiddleware(Request $request, Router $router): void
{
$router->debug('Loading middlewares');
foreach ($this->getMiddlewares() as $middleware) {
if (is_object($middleware) === false) {
$middleware = $router->getClassLoader()->loadClass($middleware);
}
if (($middleware instanceof IMiddleware) === false) {
throw new HttpException($middleware . ' must be inherit the IMiddleware interface');
}
$className = get_class($middleware);
$router->debug('Loading middleware "%s"', $className);
$middleware->handle($request);
$router->debug('Finished loading middleware "%s"', $className);
}
$router->debug('Finished loading middlewares');
}
public function matchRegex(Request $request, $url): ?bool
{
/* Match on custom defined regular expression */
if ($this->regex === null) {
return null;
}
$parameters = [];
if ((bool)preg_match($this->regex, $url, $parameters) !== false) {
$this->setParameters($parameters);
return true;
}
return false;
}
/**
* Set url
*
* @param string $url
* @return static
*/
public function setUrl(string $url): ILoadableRoute
{
$this->url = ($url === '/') ? '/' : '/' . trim($url, '/') . '/';
if (strpos($this->url, $this->paramModifiers[0]) !== false) {
$regex = sprintf(static::PARAMETERS_REGEX_FORMAT, $this->paramModifiers[0], $this->paramOptionalSymbol, $this->paramModifiers[1]);
if ((bool)preg_match_all('/' . $regex . '/u', $this->url, $matches) !== false) {
$this->parameters = array_fill_keys($matches[1], null);
}
}
return $this;
}
/**
* Prepends url while ensuring that the url has the correct formatting.
*
* @param string $url
* @return ILoadableRoute
*/
public function prependUrl(string $url): ILoadableRoute
{
return $this->setUrl(rtrim($url, '/') . $this->url);
}
public function getUrl(): string
{
return $this->url;
}
/**
* Returns true if group is defined and matches the given url.
*
* @param string $url
* @param Request $request
* @return bool
*/
protected function matchGroup(string $url, Request $request): bool
{
return ($this->getGroup() === null || $this->getGroup()->matchRoute($url, $request) === true);
}
/**
* Find url that matches method, parameters or name.
* Used when calling the url() helper.
*
* @param string|null $method
* @param string|array|null $parameters
* @param string|null $name
* @return string
*/
public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string
{
$url = $this->getUrl();
$group = $this->getGroup();
if ($group !== null && count($group->getDomains()) !== 0) {
$url = '//' . $group->getDomains()[0] . $url;
}
/* Create the param string - {parameter} */
$param1 = $this->paramModifiers[0] . '%s' . $this->paramModifiers[1];
/* Create the param string with the optional symbol - {parameter?} */
$param2 = $this->paramModifiers[0] . '%s' . $this->paramOptionalSymbol . $this->paramModifiers[1];
/* Replace any {parameter} in the url with the correct value */
$params = $this->getParameters();
foreach (array_keys($params) as $param) {
if ($parameters === '' || (is_array($parameters) === true && count($parameters) === 0)) {
$value = '';
} else {
$p = (array)$parameters;
$value = array_key_exists($param, $p) ? $p[$param] : $params[$param];
/* If parameter is specifically set to null - use the original-defined value */
if ($value === null && isset($this->originalParameters[$param]) === true) {
$value = $this->originalParameters[$param];
}
}
if (stripos($url, $param1) !== false || stripos($url, $param) !== false) {
/* Add parameter to the correct position */
$url = str_ireplace([sprintf($param1, $param), sprintf($param2, $param)], (string)$value, $url);
} else {
/* Parameter aren't recognized and will be appended at the end of the url */
$url .= $value . '/';
}
}
return rtrim('/' . ltrim($url, '/'), '/') . '/';
}
/**
* Returns the provided name for the router.
*
* @return string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Check if route has given name.
*
* @param string $name
* @return bool
*/
public function hasName(string $name): bool
{
return strtolower((string)$this->name) === strtolower((string)$name);
}
/**
* Add regular expression match for the entire route.
*
* @param string $regex
* @return static
*/
public function setMatch(string $regex): ILoadableRoute
{
$this->regex = $regex;
return $this;
}
/**
* Get regular expression match used for matching route (if defined).
*
* @return string
*/
public function getMatch(): string
{
return $this->regex;
}
/**
* Sets the router name, which makes it easier to obtain the url or router at a later point.
* Alias for LoadableRoute::setName().
*
* @param string|array $name
* @return static
* @see LoadableRoute::setName()
*/
public function name($name): ILoadableRoute
{
return $this->setName($name);
}
/**
* Sets the router name, which makes it easier to obtain the url or router at a later point.
*
* @param string $name
* @return static
*/
public function setName(string $name): ILoadableRoute
{
$this->name = $name;
return $this;
}
/**
* Merge with information from another route.
*
* @param array $settings
* @param bool $merge
* @return static
*/
public function setSettings(array $settings, bool $merge = false): IRoute
{
if (isset($settings['as']) === true) {
$name = $settings['as'];
if ($this->name !== null && $merge !== false) {
$name .= '.' . $this->name;
}
$this->setName($name);
}
if (isset($settings['prefix']) === true) {
$this->prependUrl($settings['prefix']);
}
return parent::setSettings($settings, $merge);
}
}
-619
View File
@@ -1,619 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Exceptions\ClassNotFoundHttpException;
use Pecee\SimpleRouter\Exceptions\NotFoundHttpException;
use Pecee\SimpleRouter\Router;
abstract class Route implements IRoute
{
protected const PARAMETERS_REGEX_FORMAT = '%s([\w]+)(\%s?)%s';
protected const PARAMETERS_DEFAULT_REGEX = '[\w-]+';
/**
* If enabled parameters containing null-value
* will not be passed along to the callback.
*
* @var bool
*/
protected $filterEmptyParams = true;
/**
* Default regular expression used for parsing parameters.
* @var string|null
*/
protected $defaultParameterRegex;
protected $paramModifiers = '{}';
protected $paramOptionalSymbol = '?';
protected $urlRegex = '/^%s\/?$/u';
protected $group;
protected $parent;
/**
* @var string|callable|null
*/
protected $callback;
protected $defaultNamespace;
/* Default options */
protected $namespace;
protected $requestMethods = [];
protected $where = [];
protected $parameters = [];
protected $originalParameters = [];
protected $middlewares = [];
/**
* Render route
*
* @param Request $request
* @param Router $router
* @return string|null
* @throws NotFoundHttpException
*/
public function renderRoute(Request $request, Router $router): ?string
{
$router->debug('Starting rendering route "%s"', get_class($this));
$callback = $this->getCallback();
if ($callback === null) {
return null;
}
$router->debug('Parsing parameters');
$parameters = $this->getParameters();
$router->debug('Finished parsing parameters');
/* Filter parameters with null-value */
if ($this->filterEmptyParams === true) {
$parameters = array_filter($parameters, static function ($var): bool {
return ($var !== null);
});
}
/* Render callback function */
if (is_callable($callback) === true) {
$router->debug('Executing callback');
/* Load class from type hinting */
if (is_array($callback) === true && isset($callback[0], $callback[1]) === true) {
$callback[0] = $router->getClassLoader()->loadClass($callback[0]);
}
/* When the callback is a function */
return $router->getClassLoader()->loadClosure($callback, $parameters);
}
$controller = $this->getClass();
$method = $this->getMethod();
$namespace = $this->getNamespace();
$className = ($namespace !== null && $controller[0] !== '\\') ? $namespace . '\\' . $controller : $controller;
$router->debug('Loading class %s', $className);
$class = $router->getClassLoader()->loadClass($className);
if ($method === null) {
$controller[1] = '__invoke';
}
if (method_exists($class, $method) === false) {
throw new ClassNotFoundHttpException($className, $method, sprintf('Method "%s" does not exist in class "%s"', $method, $className), 404, null);
}
$router->debug('Executing callback %s -> %s', $className, $method);
return $router->getClassLoader()->loadClassMethod($class, $method, $parameters);
}
protected function parseParameters($route, $url, $parameterRegex = null): ?array
{
$regex = (strpos($route, $this->paramModifiers[0]) === false) ? null :
sprintf
(
static::PARAMETERS_REGEX_FORMAT,
$this->paramModifiers[0],
$this->paramOptionalSymbol,
$this->paramModifiers[1]
);
// Ensures that host names/domains will work with parameters
if($route[0] == '{') $url = '/' . ltrim($url, '/');
$urlRegex = '';
$parameters = [];
if ($regex === null || (bool)preg_match_all('/' . $regex . '/u', $route, $parameters) === false) {
$urlRegex = preg_quote($route, '/');
} else {
foreach (preg_split('/((\.?-?\/?){[^}]+})/', $route) as $key => $t) {
$regex = '';
if ($key < count($parameters[1])) {
$name = $parameters[1][$key];
/* If custom regex is defined, use that */
if (isset($this->where[$name]) === true) {
$regex = $this->where[$name];
} else {
$regex = $parameterRegex ?? $this->defaultParameterRegex ?? static::PARAMETERS_DEFAULT_REGEX;
}
$regex = sprintf('((\/|-|\.)(?P<%2$s>%3$s))%1$s', $parameters[2][$key], $name, $regex);
}
$urlRegex .= preg_quote($t, '/') . $regex;
}
}
if (trim($urlRegex) === '' || (bool)preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) === false) {
return null;
}
$values = [];
if (isset($parameters[1]) === true) {
$groupParameters = $this->getGroup() !== null ? $this->getGroup()->getParameters() : [];
$lastParams = [];
/* Only take matched parameters with name */
foreach ((array)$parameters[1] as $name) {
// Ignore parent parameters
if (isset($groupParameters[$name]) === true) {
$lastParams[$name] = $matches[$name];
continue;
}
$values[$name] = (isset($matches[$name]) === true && $matches[$name] !== '') ? $matches[$name] : null;
}
$values = array_merge($values, $lastParams);
}
$this->originalParameters = $values;
return $values;
}
/**
* Returns callback name/identifier for the current route based on the callback.
* Useful if you need to get a unique identifier for the loaded route, for instance
* when using translations etc.
*
* @return string
*/
public function getIdentifier(): string
{
if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) {
return $this->callback;
}
return 'function:' . md5($this->callback);
}
/**
* Set allowed request methods
*
* @param array $methods
* @return static
*/
public function setRequestMethods(array $methods): IRoute
{
$this->requestMethods = $methods;
return $this;
}
/**
* Get allowed request methods
*
* @return array
*/
public function getRequestMethods(): array
{
return $this->requestMethods;
}
/**
* @return IRoute|null
*/
public function getParent(): ?IRoute
{
return $this->parent;
}
/**
* Get the group for the route.
*
* @return IGroupRoute|null
*/
public function getGroup(): ?IGroupRoute
{
return $this->group;
}
/**
* Set group
*
* @param IGroupRoute $group
* @return static
*/
public function setGroup(IGroupRoute $group): IRoute
{
$this->group = $group;
/* Add/merge parent settings with child */
return $this->setSettings($group->toArray(), true);
}
/**
* Set parent route
*
* @param IRoute $parent
* @return static
*/
public function setParent(IRoute $parent): IRoute
{
$this->parent = $parent;
return $this;
}
/**
* Set callback
*
* @param string|array|\Closure $callback
* @return static
*/
public function setCallback($callback): IRoute
{
$this->callback = $callback;
return $this;
}
/**
* @return string|callable|null
*/
public function getCallback()
{
return $this->callback;
}
public function getMethod(): ?string
{
if (is_array($this->callback) === true && count($this->callback) > 1) {
return $this->callback[1];
}
if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) {
$tmp = explode('@', $this->callback);
return $tmp[1];
}
return null;
}
public function getClass(): ?string
{
if (is_array($this->callback) === true && count($this->callback) > 0) {
return $this->callback[0];
}
if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) {
$tmp = explode('@', $this->callback);
return $tmp[0];
}
return null;
}
public function setMethod(string $method): IRoute
{
$this->callback = [$this->getClass(), $method];
return $this;
}
public function setClass(string $class): IRoute
{
$this->callback = [$class, $this->getMethod()];
return $this;
}
/**
* @param string $namespace
* @return static
*/
public function setNamespace(string $namespace): IRoute
{
// Do not set namespace when class-hinting is used
if (is_array($this->callback) === true) {
return $this;
}
$ns = $this->getNamespace();
if ($ns !== null) {
// Don't overwrite namespaces that starts with \
if ($ns[0] !== '\\') {
$namespace .= '\\' . $ns;
} else {
$namespace = $ns;
}
}
$this->namespace = $namespace;
return $this;
}
/**
* @param string $namespace
* @return static
*/
public function setDefaultNamespace(string $namespace): IRoute
{
$this->defaultNamespace = $namespace;
return $this;
}
public function getDefaultNamespace(): ?string
{
return $this->defaultNamespace;
}
/**
* @return string|null
*/
public function getNamespace(): ?string
{
return $this->namespace ?? $this->defaultNamespace;
}
/**
* Export route settings to array so they can be merged with another route.
*
* @return array
*/
public function toArray(): array
{
$values = [];
if ($this->namespace !== null) {
$values['namespace'] = $this->namespace;
}
if (count($this->requestMethods) !== 0) {
$values['method'] = $this->requestMethods;
}
if (count($this->where) !== 0) {
$values['where'] = $this->where;
}
if (count($this->middlewares) !== 0) {
$values['middleware'] = $this->middlewares;
}
if ($this->defaultParameterRegex !== null) {
$values['defaultParameterRegex'] = $this->defaultParameterRegex;
}
return $values;
}
/**
* Merge with information from another route.
*
* @param array $settings
* @param bool $merge
* @return static
*/
public function setSettings(array $settings, bool $merge = false): IRoute
{
if (isset($settings['namespace']) === true) {
$this->setNamespace($settings['namespace']);
}
if (isset($settings['method']) === true) {
$this->setRequestMethods(array_merge($this->requestMethods, (array)$settings['method']));
}
if (isset($settings['where']) === true) {
$this->setWhere(array_merge($this->where, (array)$settings['where']));
}
if (isset($settings['parameters']) === true) {
$this->setParameters(array_merge($this->parameters, (array)$settings['parameters']));
}
// Push middleware if multiple
if (isset($settings['middleware']) === true) {
$this->setMiddlewares(array_merge((array)$settings['middleware'], $this->middlewares));
}
if (isset($settings['defaultParameterRegex']) === true) {
$this->setDefaultParameterRegex($settings['defaultParameterRegex']);
}
return $this;
}
/**
* Get parameter names.
*
* @return array
*/
public function getWhere(): array
{
return $this->where;
}
/**
* Set parameter names.
*
* @param array $options
* @return static
*/
public function setWhere(array $options): IRoute
{
$this->where = $options;
return $this;
}
/**
* Add regular expression parameter match.
* Alias for LoadableRoute::where()
*
* @param array $options
* @return static
* @see LoadableRoute::where()
*/
public function where(array $options)
{
return $this->setWhere($options);
}
/**
* Get parameters
*
* @return array
*/
public function getParameters(): array
{
/* Sort the parameters after the user-defined param order, if any */
$parameters = [];
if (count($this->originalParameters) !== 0) {
$parameters = $this->originalParameters;
}
return array_merge($parameters, $this->parameters);
}
/**
* Get parameters
*
* @param array $parameters
* @return static
*/
public function setParameters(array $parameters): IRoute
{
$this->parameters = array_merge($this->parameters, $parameters);
return $this;
}
/**
* Add middleware class-name
*
* @param string $middleware
* @return static
* @deprecated This method is deprecated and will be removed in the near future.
*/
public function setMiddleware(string $middleware): self
{
$this->middlewares[] = $middleware;
return $this;
}
/**
* Add middleware class-name
*
* @param string $middleware
* @return static
*/
public function addMiddleware(string $middleware): IRoute
{
$this->middlewares[] = $middleware;
return $this;
}
/**
* Set middlewares array
*
* @param array $middlewares
* @return static
*/
public function setMiddlewares(array $middlewares): IRoute
{
$this->middlewares = $middlewares;
return $this;
}
/**
* @return array
*/
public function getMiddlewares(): array
{
return $this->middlewares;
}
/**
* Set default regular expression used when matching parameters.
* This is used when no custom parameter regex is found.
*
* @param string $regex
* @return static
*/
public function setDefaultParameterRegex(string $regex): self
{
$this->defaultParameterRegex = $regex;
return $this;
}
/**
* Get default regular expression used when matching parameters.
*
* @return string
*/
public function getDefaultParameterRegex(): string
{
return $this->defaultParameterRegex;
}
/**
* If enabled parameters containing null-value will not be passed along to the callback.
*
* @param bool $enabled
* @return static $this
*/
public function setFilterEmptyParams(bool $enabled): IRoute
{
$this->filterEmptyParams = $enabled;
return $this;
}
/**
* Status if filtering of empty params is enabled or disabled
* @return bool
*/
public function getFilterEmptyParams(): bool
{
return $this->filterEmptyParams;
}
}
@@ -1,183 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
class RouteController extends LoadableRoute implements IControllerRoute
{
protected $defaultMethod = 'index';
protected $controller;
protected $method;
protected $names = [];
public function __construct($url, $controller)
{
$this->setUrl($url);
$this->setName(trim(str_replace('/', '.', $url), '/'));
$this->controller = $controller;
}
/**
* Check if route has given name.
*
* @param string $name
* @return bool
*/
public function hasName(string $name): bool
{
if ($this->name === null) {
return false;
}
/* Remove method/type */
if (strpos($name, '.') !== false) {
$method = substr($name, strrpos($name, '.') + 1);
$newName = substr($name, 0, strrpos($name, '.'));
if (in_array($method, $this->names, true) === true && strtolower($this->name) === strtolower($newName)) {
return true;
}
}
return parent::hasName($name);
}
/**
* @param string|null $method
* @param string|array|null $parameters
* @param string|null $name
* @return string
*/
public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string
{
if (strpos($name, '.') !== false) {
$found = array_search(substr($name, strrpos($name, '.') + 1), $this->names, true);
if ($found !== false) {
$method = (string)$found;
}
}
$url = '';
$parameters = (array)$parameters;
if ($method !== null) {
/* Remove requestType from method-name, if it exists */
foreach (Request::$requestTypes as $requestType) {
if (stripos($method, $requestType) === 0) {
$method = substr($method, strlen($requestType));
break;
}
}
$method .= '/';
}
$group = $this->getGroup();
if ($group !== null && count($group->getDomains()) !== 0) {
$url .= '//' . $group->getDomains()[0];
}
$url .= '/' . trim($this->getUrl(), '/') . '/' . strtolower((string)$method) . implode('/', $parameters);
return '/' . trim($url, '/') . '/';
}
public function matchRoute(string $url, Request $request): bool
{
if ($this->matchGroup($url, $request) === false) {
return false;
}
/* Match global regular-expression for route */
$regexMatch = $this->matchRegex($request, $url);
if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) {
return false;
}
$strippedUrl = trim(str_ireplace($this->url, '/', $url), '/');
$path = explode('/', $strippedUrl);
if (count($path) !== 0) {
$method = (isset($path[0]) === false || trim($path[0]) === '') ? $this->defaultMethod : $path[0];
$this->method = $request->getMethod() . ucfirst($method);
$this->parameters = array_slice($path, 1);
// Set callback
$this->setCallback([$this->controller, $this->method]);
return true;
}
return false;
}
/**
* Get controller class-name.
*
* @return string
*/
public function getController(): string
{
return $this->controller;
}
/**
* Get controller class-name.
*
* @param string $controller
* @return static
*/
public function setController(string $controller): IControllerRoute
{
$this->controller = $controller;
return $this;
}
/**
* Return active method
*
* @return string|null
*/
public function getMethod(): ?string
{
return $this->method;
}
/**
* Set active method
*
* @param string $method
* @return static
*/
public function setMethod(string $method): IRoute
{
$this->method = $method;
return $this;
}
/**
* Merge with information from another route.
*
* @param array $settings
* @param bool $merge
* @return static
*/
public function setSettings(array $settings, bool $merge = false): IRoute
{
if (isset($settings['names']) === true) {
$this->names = $settings['names'];
}
return parent::setSettings($settings, $merge);
}
}
-265
View File
@@ -1,265 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Handlers\IExceptionHandler;
class RouteGroup extends Route implements IGroupRoute
{
protected $urlRegex = '/^%s\/?/u';
protected $prefix;
protected $name;
protected $domains = [];
protected $exceptionHandlers = [];
protected $mergeExceptionHandlers = true;
/**
* Method called to check if a domain matches
*
* @param Request $request
* @return bool
*/
public function matchDomain(Request $request): bool
{
if ($this->domains === null || count($this->domains) === 0) {
return true;
}
foreach ($this->domains as $domain) {
// If domain has no parameters but matches
if ($domain === $request->getHost()) {
return true;
}
$parameters = $this->parseParameters($domain, $request->getHost(), '.*');
if ($parameters !== null && count($parameters) !== 0) {
$this->parameters = $parameters;
return true;
}
}
return false;
}
/**
* Method called to check if route matches
*
* @param string $url
* @param Request $request
* @return bool
*/
public function matchRoute(string $url, Request $request): bool
{
if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) {
return false;
}
if ($this->prefix !== null) {
/* Parse parameters from current route */
$parameters = $this->parseParameters($this->prefix, $url);
/* If no custom regular expression or parameters was found on this route, we stop */
if ($parameters === null) {
return false;
}
/* Set the parameters */
$this->setParameters($parameters);
}
$parsedPrefix = $this->prefix;
foreach ($this->getParameters() as $parameter => $value) {
$parsedPrefix = str_ireplace('{' . $parameter . '}', (string)$value, (string)$parsedPrefix);
}
/* Skip if prefix doesn't match */
if ($this->prefix !== null && stripos($url, rtrim($parsedPrefix, '/') . '/') === false) {
return false;
}
return $this->matchDomain($request);
}
/**
* Add exception handler
*
* @param IExceptionHandler|string $handler
* @return static
*/
public function addExceptionHandler($handler): IGroupRoute
{
$this->exceptionHandlers[] = $handler;
return $this;
}
/**
* Set exception-handlers for group
*
* @param array $handlers
* @return static
*/
public function setExceptionHandlers(array $handlers): IGroupRoute
{
$this->exceptionHandlers = $handlers;
return $this;
}
/**
* Get exception-handlers for group
*
* @return array
*/
public function getExceptionHandlers(): array
{
return $this->exceptionHandlers;
}
/**
* Get allowed domains for domain.
*
* @return array
*/
public function getDomains(): array
{
return $this->domains;
}
/**
* Set allowed domains for group.
*
* @param array $domains
* @return static
*/
public function setDomains(array $domains): IGroupRoute
{
$this->domains = $domains;
return $this;
}
/**
* @param string $prefix
* @return static
*/
public function setPrefix(string $prefix): IGroupRoute
{
$this->prefix = '/' . trim($prefix, '/');
return $this;
}
/**
* Prepends prefix while ensuring that the url has the correct formatting.
*
* @param string $url
* @return static
*/
public function prependPrefix(string $url): IGroupRoute
{
return $this->setPrefix(rtrim($url, '/') . $this->prefix);
}
/**
* Set prefix that child-routes will inherit.
*
* @return string|null
*/
public function getPrefix(): ?string
{
return $this->prefix;
}
/**
* When enabled group will overwrite any existing exception-handlers.
*
* @param bool $merge
* @return static
*/
public function setMergeExceptionHandlers(bool $merge): IGroupRoute
{
$this->mergeExceptionHandlers = $merge;
return $this;
}
/**
* Returns true if group should overwrite existing exception-handlers.
*
* @return bool
*/
public function getMergeExceptionHandlers(): bool
{
return $this->mergeExceptionHandlers;
}
/**
* Merge with information from another route.
*
* @param array $settings
* @param bool $merge
* @return static
*/
public function setSettings(array $settings, bool $merge = false): IRoute
{
if (isset($settings['prefix']) === true) {
$this->setPrefix($settings['prefix'] . $this->prefix);
}
if (isset($settings['mergeExceptionHandlers']) === true) {
$this->setMergeExceptionHandlers($settings['mergeExceptionHandlers']);
}
if ($merge === false && isset($settings['exceptionHandler']) === true) {
$this->setExceptionHandlers((array)$settings['exceptionHandler']);
}
if ($merge === false && isset($settings['domain']) === true) {
$this->setDomains((array)$settings['domain']);
}
if (isset($settings['as']) === true) {
$name = $settings['as'];
if ($this->name !== null && $merge !== false) {
$name .= '.' . $this->name;
}
$this->name = $name;
}
return parent::setSettings($settings, $merge);
}
/**
* Export route settings to array so they can be merged with another route.
*
* @return array
*/
public function toArray(): array
{
$values = [];
if ($this->prefix !== null) {
$values['prefix'] = $this->getPrefix();
}
if ($this->name !== null) {
$values['as'] = $this->name;
}
if (count($this->parameters) !== 0) {
$values['parameters'] = $this->parameters;
}
return array_merge($values, parent::toArray());
}
}
@@ -1,7 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
class RoutePartialGroup extends RouteGroup implements IPartialGroupRoute
{
}
@@ -1,230 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
class RouteResource extends LoadableRoute implements IControllerRoute
{
protected $urls = [
'index' => '',
'create' => 'create',
'store' => '',
'show' => '',
'edit' => 'edit',
'update' => '',
'destroy' => '',
];
protected $methodNames = [
'index' => 'index',
'create' => 'create',
'store' => 'store',
'show' => 'show',
'edit' => 'edit',
'update' => 'update',
'destroy' => 'destroy',
];
protected $names = [];
protected $controller;
public function __construct($url, $controller)
{
$this->setUrl($url);
$this->controller = $controller;
$this->setName(trim(str_replace('/', '.', $url), '/'));
}
/**
* Check if route has given name.
*
* @param string $name
* @return bool
*/
public function hasName(string $name): bool
{
if ($this->name === null) {
return false;
}
if (strtolower($this->name) === strtolower($name)) {
return true;
}
/* Remove method/type */
if (strpos($name, '.') !== false) {
$name = substr($name, 0, strrpos($name, '.'));
}
return (strtolower($this->name) === strtolower($name));
}
/**
* @param string|null $method
* @param array|string|null $parameters
* @param string|null $name
* @return string
*/
public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string
{
$url = array_search($name, $this->names, true);
if ($url !== false) {
return rtrim($this->url . $this->urls[$url], '/') . '/';
}
return $this->url;
}
protected function call($method): bool
{
$this->setCallback([$this->controller, $method]);
return true;
}
public function matchRoute(string $url, Request $request): bool
{
if ($this->matchGroup($url, $request) === false) {
return false;
}
/* Match global regular-expression for route */
$regexMatch = $this->matchRegex($request, $url);
if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) {
return false;
}
$route = rtrim($this->url, '/') . '/{id?}/{action?}';
/* Parse parameters from current route */
$this->parameters = $this->parseParameters($route, $url);
/* If no custom regular expression or parameters was found on this route, we stop */
if ($regexMatch === null && $this->parameters === null) {
return false;
}
$action = strtolower(trim((string)$this->parameters['action']));
$id = $this->parameters['id'];
// Remove action parameter
unset($this->parameters['action']);
$method = $request->getMethod();
// Delete
if ($method === Request::REQUEST_TYPE_DELETE && $id !== null) {
return $this->call($this->methodNames['destroy']);
}
// Update
if ($id !== null && in_array($method, [Request::REQUEST_TYPE_PATCH, Request::REQUEST_TYPE_PUT], true) === true) {
return $this->call($this->methodNames['update']);
}
// Edit
if ($method === Request::REQUEST_TYPE_GET && $id !== null && $action === 'edit') {
return $this->call($this->methodNames['edit']);
}
// Create
if ($method === Request::REQUEST_TYPE_GET && $id === 'create') {
return $this->call($this->methodNames['create']);
}
// Save
if ($method === Request::REQUEST_TYPE_POST) {
return $this->call($this->methodNames['store']);
}
// Show
if ($method === Request::REQUEST_TYPE_GET && $id !== null) {
return $this->call($this->methodNames['show']);
}
// Index
return $this->call($this->methodNames['index']);
}
/**
* @return string
*/
public function getController(): string
{
return $this->controller;
}
/**
* @param string $controller
* @return static
*/
public function setController(string $controller): IControllerRoute
{
$this->controller = $controller;
return $this;
}
public function setName(string $name): ILoadableRoute
{
$this->name = $name;
$this->names = [
'index' => $this->name . '.index',
'create' => $this->name . '.create',
'store' => $this->name . '.store',
'show' => $this->name . '.show',
'edit' => $this->name . '.edit',
'update' => $this->name . '.update',
'destroy' => $this->name . '.destroy',
];
return $this;
}
/**
* Define custom method name for resource controller
*
* @param array $names
* @return static $this
*/
public function setMethodNames(array $names): RouteResource
{
$this->methodNames = $names;
return $this;
}
/**
* Get method names
*
* @return array
*/
public function getMethodNames(): array
{
return $this->methodNames;
}
/**
* Merge with information from another route.
*
* @param array $settings
* @param bool $merge
* @return static
*/
public function setSettings(array $settings, bool $merge = false): IRoute
{
if (isset($settings['names']) === true) {
$this->names = $settings['names'];
}
if (isset($settings['methods']) === true) {
$this->methodNames = $settings['methods'];
}
return parent::setSettings($settings, $merge);
}
}
-47
View File
@@ -1,47 +0,0 @@
<?php
namespace Pecee\SimpleRouter\Route;
use Pecee\Http\Request;
class RouteUrl extends LoadableRoute
{
/**
* RouteUrl constructor.
* @param string $url
* @param \Closure|string $callback
*/
public function __construct(string $url, $callback)
{
$this->setUrl($url);
$this->setCallback($callback);
}
public function matchRoute(string $url, Request $request): bool
{
if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) {
return false;
}
/* Match global regular-expression for route */
$regexMatch = $this->matchRegex($request, $url);
if ($regexMatch === false) {
return false;
}
/* Parse parameters from current route */
$parameters = $this->parseParameters($this->url, $url);
/* If no custom regular expression or parameters was found on this route, we stop */
if ($regexMatch === null && $parameters === null) {
return false;
}
/* Set the parameters */
$this->setParameters((array)$parameters);
return true;
}
}
-972
View File
@@ -1,972 +0,0 @@
<?php
namespace Pecee\SimpleRouter;
use Exception;
use Pecee\Exceptions\InvalidArgumentException;
use Pecee\Http\Exceptions\MalformedUrlException;
use Pecee\Http\Middleware\BaseCsrfVerifier;
use Pecee\Http\Request;
use Pecee\Http\Url;
use Pecee\SimpleRouter\ClassLoader\ClassLoader;
use Pecee\SimpleRouter\ClassLoader\IClassLoader;
use Pecee\SimpleRouter\Exceptions\HttpException;
use Pecee\SimpleRouter\Exceptions\NotFoundHttpException;
use Pecee\SimpleRouter\Handlers\EventHandler;
use Pecee\SimpleRouter\Handlers\IEventHandler;
use Pecee\SimpleRouter\Handlers\IExceptionHandler;
use Pecee\SimpleRouter\Route\IControllerRoute;
use Pecee\SimpleRouter\Route\IGroupRoute;
use Pecee\SimpleRouter\Route\ILoadableRoute;
use Pecee\SimpleRouter\Route\IPartialGroupRoute;
use Pecee\SimpleRouter\Route\IRoute;
class Router
{
/**
* Current request
* @var Request
*/
protected $request;
/**
* Defines if a route is currently being processed.
* @var bool
*/
protected $isProcessingRoute;
/**
* Defines all data from current processing route.
* @var ILoadableRoute
*/
protected $currentProcessingRoute;
/**
* All added routes
* @var array
*/
protected $routes = [];
/**
* List of processed routes
* @var array|ILoadableRoute[]
*/
protected $processedRoutes = [];
/**
* Stack of routes used to keep track of sub-routes added
* when a route is being processed.
* @var array
*/
protected $routeStack = [];
/**
* List of added bootmanagers
* @var array
*/
protected $bootManagers = [];
/**
* Csrf verifier class
* @var BaseCsrfVerifier|null
*/
protected $csrfVerifier;
/**
* Get exception handlers
* @var array
*/
protected $exceptionHandlers = [];
/**
* List of loaded exception that has been loaded.
* Used to ensure that exception-handlers aren't loaded twice when rewriting route.
*
* @var array
*/
protected $loadedExceptionHandlers = [];
/**
* Enable or disabled debugging
* @var bool
*/
protected $debugEnabled = false;
/**
* The start time used when debugging is enabled
* @var float
*/
protected $debugStartTime;
/**
* List containing all debug messages
* @var array
*/
protected $debugList = [];
/**
* Contains any registered event-handler.
* @var array
*/
protected $eventHandlers = [];
/**
* Class loader instance
* @var IClassLoader
*/
protected $classLoader;
/**
* When enabled the router will render all routes that matches.
* When disabled the router will stop execution when first route is found.
* @var bool
*/
protected $renderMultipleRoutes = true;
/**
* Router constructor.
*/
public function __construct()
{
$this->reset();
}
/**
* Resets the router by reloading request and clearing all routes and data.
*/
public function reset(): void
{
$this->debugStartTime = microtime(true);
$this->isProcessingRoute = false;
try {
$this->request = new Request();
} catch (MalformedUrlException $e) {
$this->debug(sprintf('Invalid request-uri url: %s', $e->getMessage()));
}
$this->routes = [];
$this->bootManagers = [];
$this->routeStack = [];
$this->processedRoutes = [];
$this->exceptionHandlers = [];
$this->loadedExceptionHandlers = [];
$this->eventHandlers = [];
$this->debugList = [];
$this->csrfVerifier = null;
$this->classLoader = new ClassLoader();
}
/**
* Add route
* @param IRoute $route
* @return IRoute
*/
public function addRoute(IRoute $route): IRoute
{
$this->fireEvents(EventHandler::EVENT_ADD_ROUTE, [
'route' => $route,
'isSubRoute' => $this->isProcessingRoute,
]);
/*
* If a route is currently being processed, that means that the route being added are rendered from the parent
* routes callback, so we add them to the stack instead.
*/
if ($this->isProcessingRoute === true) {
$this->routeStack[] = $route;
} else {
$this->routes[] = $route;
}
return $route;
}
/**
* Render and process any new routes added.
*
* @param IRoute $route
* @throws NotFoundHttpException
*/
protected function renderAndProcess(IRoute $route): void
{
$this->isProcessingRoute = true;
$route->renderRoute($this->request, $this);
$this->isProcessingRoute = false;
if (count($this->routeStack) !== 0) {
/* Pop and grab the routes added when executing group callback earlier */
$stack = $this->routeStack;
$this->routeStack = [];
/* Route any routes added to the stack */
$this->processRoutes($stack, ($route instanceof IGroupRoute) ? $route : null);
}
}
/**
* Process added routes.
*
* @param array|IRoute[] $routes
* @param IGroupRoute|null $group
* @throws NotFoundHttpException
*/
protected function processRoutes(array $routes, ?IGroupRoute $group = null): void
{
$this->debug('Processing routes');
// Stop processing routes if no valid route is found.
if ($this->request->getRewriteRoute() === null && $this->request->getUrl()->getOriginalUrl() === '') {
$this->debug('Halted route-processing as no valid route was found');
return;
}
$url = $this->request->getRewriteUrl() ?? $this->request->getUrl()->getPath();
// Loop through each route-request
foreach ($routes as $route) {
$this->debug('Processing route "%s"', get_class($route));
if ($group !== null) {
/* Add the parent group */
$route->setGroup($group);
}
/* @var $route IGroupRoute */
if ($route instanceof IGroupRoute) {
if ($route->matchRoute($url, $this->request) === true) {
/* Add exception handlers */
if (count($route->getExceptionHandlers()) !== 0) {
if ($route->getMergeExceptionHandlers() === true) {
foreach ($route->getExceptionHandlers() as $handler) {
$this->exceptionHandlers[] = $handler;
}
} else {
$this->exceptionHandlers = $route->getExceptionHandlers();
}
}
/* Only render partial group if it matches */
if ($route instanceof IPartialGroupRoute === true) {
$this->renderAndProcess($route);
continue;
}
}
if ($route instanceof IPartialGroupRoute === false) {
$this->renderAndProcess($route);
}
continue;
}
if ($route instanceof ILoadableRoute === true) {
/* Add the route to the map, so we can find the active one when all routes has been loaded */
$this->processedRoutes[] = $route;
}
}
}
/**
* Load routes
* @return void
* @throws NotFoundHttpException
*/
public function loadRoutes(): void
{
$this->debug('Loading routes');
$this->fireEvents(EventHandler::EVENT_LOAD_ROUTES, [
'routes' => $this->routes,
]);
/* Loop through each route-request */
$this->processRoutes($this->routes);
$this->fireEvents(EventHandler::EVENT_BOOT, [
'bootmanagers' => $this->bootManagers,
]);
/* Initialize boot-managers */
/* @var $manager IRouterBootManager */
foreach ($this->bootManagers as $manager) {
$className = get_class($manager);
$this->debug('Rendering bootmanager "%s"', $className);
$this->fireEvents(EventHandler::EVENT_RENDER_BOOTMANAGER, [
'bootmanagers' => $this->bootManagers,
'bootmanager' => $manager,
]);
/* Render bootmanager */
$manager->boot($this, $this->request);
$this->debug('Finished rendering bootmanager "%s"', $className);
}
$this->debug('Finished loading routes');
}
/**
* Start the routing
*
* @return string|null
* @throws NotFoundHttpException
* @throws \Pecee\Http\Middleware\Exceptions\TokenMismatchException
* @throws HttpException
* @throws Exception
*/
public function start(): ?string
{
$this->debug('Router starting');
$this->fireEvents(EventHandler::EVENT_INIT);
$this->loadRoutes();
if ($this->csrfVerifier !== null) {
$this->fireEvents(EventHandler::EVENT_RENDER_CSRF, [
'csrfVerifier' => $this->csrfVerifier,
]);
/* Verify csrf token for request */
$this->csrfVerifier->handle($this->request);
}
$output = $this->routeRequest();
$this->fireEvents(EventHandler::EVENT_LOAD, [
'loadedRoutes' => $this->getRequest()->getLoadedRoutes(),
]);
$this->debug('Routing complete');
return $output;
}
/**
* Routes the request
*
* @return string|null
* @throws HttpException
* @throws Exception
*/
public function routeRequest(): ?string
{
$this->debug('Routing request');
$methodNotAllowed = null;
try {
$url = $this->request->getRewriteUrl() ?? $this->request->getUrl()->getPath();
/* @var $route ILoadableRoute */
foreach ($this->processedRoutes as $key => $route) {
$this->debug('Matching route "%s"', get_class($route));
/* Add current processing route to constants */
$this->currentProcessingRoute = $route;
/* If the route matches */
if ($route->matchRoute($url, $this->request) === true) {
$this->fireEvents(EventHandler::EVENT_MATCH_ROUTE, [
'route' => $route,
]);
/* Check if request method matches */
if (count($route->getRequestMethods()) !== 0 && in_array($this->request->getMethod(), $route->getRequestMethods(), true) === false) {
$this->debug('Method "%s" not allowed', $this->request->getMethod());
// Only set method not allowed is not already set
if ($methodNotAllowed === null) {
$methodNotAllowed = true;
}
continue;
}
$this->fireEvents(EventHandler::EVENT_RENDER_MIDDLEWARES, [
'route' => $route,
'middlewares' => $route->getMiddlewares(),
]);
$route->loadMiddleware($this->request, $this);
$output = $this->handleRouteRewrite($key, $url);
if ($output !== null) {
return $output;
}
$methodNotAllowed = false;
$this->request->addLoadedRoute($route);
$this->fireEvents(EventHandler::EVENT_RENDER_ROUTE, [
'route' => $route,
]);
$routeOutput = $route->renderRoute($this->request, $this);
if ($this->renderMultipleRoutes === true) {
if ($routeOutput !== null) {
return $routeOutput;
}
$output = $this->handleRouteRewrite($key, $url);
if ($output !== null) {
return $output;
}
} else {
$output = $this->handleRouteRewrite($key, $url);
return $output ?? $routeOutput;
}
}
}
} catch (Exception $e) {
$this->handleException($e);
}
if ($methodNotAllowed === true) {
$message = sprintf('Route "%s" or method "%s" not allowed.', $this->request->getUrl()->getPath(), $this->request->getMethod());
$this->handleException(new NotFoundHttpException($message, 403));
}
if (count($this->request->getLoadedRoutes()) === 0) {
$rewriteUrl = $this->request->getRewriteUrl();
if ($rewriteUrl !== null) {
$message = sprintf('Route not found: "%s" (rewrite from: "%s")', $rewriteUrl, $this->request->getUrl()->getPath());
} else {
$message = sprintf('Route not found: "%s"', $this->request->getUrl()->getPath());
}
$this->debug($message);
return $this->handleException(new NotFoundHttpException($message, 404));
}
return null;
}
/**
* Handle route-rewrite
*
* @param string $key
* @param string $url
* @return string|null
* @throws HttpException
* @throws Exception
*/
protected function handleRouteRewrite(string $key, string $url): ?string
{
/* If the request has changed */
if ($this->request->hasPendingRewrite() === false) {
return null;
}
$route = $this->request->getRewriteRoute();
if ($route !== null) {
/* Add rewrite route */
$this->processedRoutes[] = $route;
}
if ($this->request->getRewriteUrl() !== $url) {
unset($this->processedRoutes[$key]);
$this->request->setHasPendingRewrite(false);
$this->fireEvents(EventHandler::EVENT_REWRITE, [
'rewriteUrl' => $this->request->getRewriteUrl(),
'rewriteRoute' => $this->request->getRewriteRoute(),
]);
return $this->routeRequest();
}
return null;
}
/**
* @param Exception $e
* @return string|null
* @throws Exception
* @throws HttpException
*/
protected function handleException(Exception $e): ?string
{
$this->debug('Starting exception handling for "%s"', get_class($e));
$this->fireEvents(EventHandler::EVENT_LOAD_EXCEPTIONS, [
'exception' => $e,
'exceptionHandlers' => $this->exceptionHandlers,
]);
/* @var $handler IExceptionHandler */
foreach (array_reverse($this->exceptionHandlers) as $key => $handler) {
if (is_object($handler) === false) {
$handler = new $handler();
}
$this->fireEvents(EventHandler::EVENT_RENDER_EXCEPTION, [
'exception' => $e,
'exceptionHandler' => $handler,
'exceptionHandlers' => $this->exceptionHandlers,
]);
$this->debug('Processing exception-handler "%s"', get_class($handler));
if (($handler instanceof IExceptionHandler) === false) {
throw new HttpException('Exception handler must implement the IExceptionHandler interface.', 500);
}
try {
$this->debug('Start rendering exception handler');
$handler->handleError($this->request, $e);
$this->debug('Finished rendering exception-handler');
if (isset($this->loadedExceptionHandlers[$key]) === false && $this->request->hasPendingRewrite() === true) {
$this->loadedExceptionHandlers[$key] = $handler;
$this->debug('Exception handler contains rewrite, reloading routes');
$this->fireEvents(EventHandler::EVENT_REWRITE, [
'rewriteUrl' => $this->request->getRewriteUrl(),
'rewriteRoute' => $this->request->getRewriteRoute(),
]);
if ($this->request->getRewriteRoute() !== null) {
$this->processedRoutes[] = $this->request->getRewriteRoute();
}
return $this->routeRequest();
}
} catch (Exception $e) {
}
$this->debug('Finished processing');
}
$this->debug('Finished exception handling - exception not handled, throwing');
throw $e;
}
/**
* Find route by alias, class, callback or method.
*
* @param string $name
* @return ILoadableRoute|null
*/
public function findRoute(string $name): ?ILoadableRoute
{
$this->debug('Finding route by name "%s"', $name);
$this->fireEvents(EventHandler::EVENT_FIND_ROUTE, [
'name' => $name,
]);
foreach ($this->processedRoutes as $route) {
/* Check if the name matches with a name on the route. Should match either router alias or controller alias. */
if ($route->hasName($name) === true) {
$this->debug('Found route "%s" by name "%s"', $route->getUrl(), $name);
return $route;
}
/* Direct match to controller */
if ($route instanceof IControllerRoute && strtoupper($route->getController()) === strtoupper($name)) {
$this->debug('Found route "%s" by controller "%s"', $route->getUrl(), $name);
return $route;
}
/* Using @ is most definitely a controller@method or alias@method */
if (strpos($name, '@') !== false) {
[$controller, $method] = array_map('strtolower', explode('@', $name));
if ($controller === strtolower((string)$route->getClass()) && $method === strtolower((string)$route->getMethod())) {
$this->debug('Found route "%s" by controller "%s" and method "%s"', $route->getUrl(), $controller, $method);
return $route;
}
}
/* Check if callback matches (if it's not a function) */
$callback = $route->getCallback();
if (is_string($callback) === true && is_callable($callback) === false && strpos($name, '@') !== false && strpos($callback, '@') !== false) {
/* Check if the entire callback is matching */
if (strpos($callback, $name) === 0 || strtolower($callback) === strtolower($name)) {
$this->debug('Found route "%s" by callback "%s"', $route->getUrl(), $name);
return $route;
}
/* Check if the class part of the callback matches (class@method) */
if (strtolower($name) === strtolower($route->getClass())) {
$this->debug('Found route "%s" by class "%s"', $route->getUrl(), $name);
return $route;
}
}
}
$this->debug('Route not found');
return null;
}
/**
* Get url for a route by using either name/alias, class or method name.
*
* The name parameter supports the following values:
* - Route name
* - Controller/resource name (with or without method)
* - Controller class name
*
* When searching for controller/resource by name, you can use this syntax "route.name@method".
* You can also use the same syntax when searching for a specific controller-class "MyController@home".
* If no arguments is specified, it will return the url for the current loaded route.
*
* @param string|null $name
* @param string|array|null $parameters
* @param array|null $getParams
* @return Url
* @throws InvalidArgumentException
*/
public function getUrl(?string $name = null, $parameters = null, ?array $getParams = null): Url
{
$this->debug('Finding url', func_get_args());
$this->fireEvents(EventHandler::EVENT_GET_URL, [
'name' => $name,
'parameters' => $parameters,
'getParams' => $getParams,
]);
if ($name === '' && $parameters === '') {
return new Url('/');
}
/* Only merge $_GET when all parameters are null */
$getParams = ($name === null && $parameters === null && $getParams === null) ? $_GET : (array)$getParams;
/* Return current route if no options has been specified */
if ($name === null && $parameters === null) {
return $this->request
->getUrlCopy()
->setParams($getParams);
}
$loadedRoute = $this->request->getLoadedRoute();
/* If nothing is defined and a route is loaded we use that */
if ($name === null && $loadedRoute !== null) {
return $this->request
->getUrlCopy()
->setPath($loadedRoute->findUrl($loadedRoute->getMethod(), $parameters, $name))
->setParams($getParams);
}
if ($name !== null) {
/* We try to find a match on the given name */
$route = $this->findRoute($name);
if ($route !== null) {
return $this->request
->getUrlCopy()
->setPath($route->findUrl($route->getMethod(), $parameters, $name))
->setParams($getParams);
}
}
/* Using @ is most definitely a controller@method or alias@method */
if (is_string($name) === true && strpos($name, '@') !== false) {
[$controller, $method] = explode('@', $name);
/* Loop through all the routes to see if we can find a match */
/* @var $route ILoadableRoute */
foreach ($this->processedRoutes as $processedRoute) {
/* Check if the route contains the name/alias */
if ($processedRoute->hasName($controller) === true) {
return $this->request
->getUrlCopy()
->setPath($processedRoute->findUrl($method, $parameters, $name))
->setParams($getParams);
}
/* Check if the route controller is equal to the name */
if ($processedRoute instanceof IControllerRoute && strtolower($processedRoute->getController()) === strtolower($controller)) {
return $this->request
->getUrlCopy()
->setPath($processedRoute->findUrl($method, $parameters, $name))
->setParams($getParams);
}
}
}
/* No result so we assume that someone is using a hardcoded url and join everything together. */
$url = trim(implode('/', array_merge((array)$name, (array)$parameters)), '/');
$url = (($url === '') ? '/' : '/' . $url . '/');
return $this->request
->getUrlCopy()
->setPath($url)
->setParams($getParams);
}
/**
* Get BootManagers
* @return array
*/
public function getBootManagers(): array
{
return $this->bootManagers;
}
/**
* Set BootManagers
*
* @param array $bootManagers
* @return static
*/
public function setBootManagers(array $bootManagers): self
{
$this->bootManagers = $bootManagers;
return $this;
}
/**
* Add BootManager
*
* @param IRouterBootManager $bootManager
* @return static
*/
public function addBootManager(IRouterBootManager $bootManager): self
{
$this->bootManagers[] = $bootManager;
return $this;
}
/**
* Get routes that has been processed.
*
* @return array
*/
public function getProcessedRoutes(): array
{
return $this->processedRoutes;
}
/**
* @return array
*/
public function getRoutes(): array
{
return $this->routes;
}
/**
* Set routes
*
* @param array $routes
* @return static
*/
public function setRoutes(array $routes): self
{
$this->routes = $routes;
return $this;
}
/**
* Get current request
*
* @return Request
*/
public function getRequest(): Request
{
return $this->request;
}
/**
* Get csrf verifier class
* @return BaseCsrfVerifier
*/
public function getCsrfVerifier(): ?BaseCsrfVerifier
{
return $this->csrfVerifier;
}
/**
* Set csrf verifier class
*
* @param BaseCsrfVerifier $csrfVerifier
*/
public function setCsrfVerifier(BaseCsrfVerifier $csrfVerifier): void
{
$this->csrfVerifier = $csrfVerifier;
}
/**
* Set class loader
*
* @param IClassLoader $loader
*/
public function setClassLoader(IClassLoader $loader): void
{
$this->classLoader = $loader;
}
/**
* Get class loader
*
* @return IClassLoader
*/
public function getClassLoader(): IClassLoader
{
return $this->classLoader;
}
/**
* Register event handler
*
* @param IEventHandler $handler
*/
public function addEventHandler(IEventHandler $handler): void
{
$this->eventHandlers[] = $handler;
}
/**
* Get registered event-handler.
*
* @return array
*/
public function getEventHandlers(): array
{
return $this->eventHandlers;
}
/**
* Fire event in event-handler.
*
* @param string $name
* @param array $arguments
*/
protected function fireEvents(string $name, array $arguments = []): void
{
if (count($this->eventHandlers) === 0) {
return;
}
/* @var IEventHandler $eventHandler */
foreach ($this->eventHandlers as $eventHandler) {
$eventHandler->fireEvents($this, $name, $arguments);
}
}
/**
* Add new debug message
* @param string $message
* @param array $args
*/
public function debug(string $message, ...$args): void
{
if ($this->debugEnabled === false) {
return;
}
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$this->debugList[] = [
'message' => vsprintf($message, $args),
'time' => number_format(microtime(true) - $this->debugStartTime, 10),
'trace' => end($trace),
];
}
/**
* Enable or disables debugging
*
* @param bool $enabled
* @return static
*/
public function setDebugEnabled(bool $enabled): self
{
$this->debugEnabled = $enabled;
return $this;
}
/**
* Get the list containing all debug messages.
*
* @return array
*/
public function getDebugLog(): array
{
return $this->debugList;
}
/**
* Get the current processing route details.
*
* @return ILoadableRoute
*/
public function getCurrentProcessingRoute(): ILoadableRoute
{
return $this->currentProcessingRoute;
}
/**
* Changes the rendering behavior of the router.
* When enabled the router will render all routes that matches.
* When disabled the router will stop rendering at the first route that matches.
*
* @param bool $bool
* @return $this
*/
public function setRenderMultipleRoutes(bool $bool): self
{
$this->renderMultipleRoutes = $bool;
return $this;
}
public function addExceptionHandler(IExceptionHandler $handler): self
{
$this->exceptionHandlers[] = $handler;
return $this;
}
}
+486
View File
@@ -0,0 +1,486 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Exception\RouterException;
use Pecee\Handler\IExceptionHandler;
use Pecee\Http\Middleware\BaseCsrfVerifier;
use Pecee\Http\Request;
class RouterBase {
protected static $instance;
protected $request;
protected $currentRoute;
protected $routes;
protected $processedRoutes;
protected $controllerUrlMap;
protected $backStack;
protected $defaultNamespace;
protected $bootManagers;
protected $baseCsrfVerifier;
protected $exceptionHandlers;
// TODO: clean up - cut some of the methods down to smaller pieces
public function __construct() {
$this->request = Request::getInstance();
$this->routes = array();
$this->backStack = array();
$this->controllerUrlMap = array();
$this->bootManagers = array();
$this->exceptionHandlers = array();
}
public function addRoute(RouterEntry $route) {
if($this->currentRoute !== null) {
$this->backStack[] = $route;
} else {
$this->routes[] = $route;
}
}
protected function processRoutes(array $routes, array $settings = array(), array $prefixes = array(), $backStack = false, $group = null) {
// Loop through each route-request
$routesCount = count($routes);
$mergedSettings = array();
/* @var $route RouterEntry */
for($i = 0; $i < $routesCount; $i++) {
$route = $routes[$i];
$route->addSettings($settings);
if($backStack) {
$route->setGroup($group);
}
if($this->defaultNamespace && !$route->getNamespace()) {
$namespace = $this->defaultNamespace;
if ($route->getNamespace()) {
$namespace .= '\\' . $route->getNamespace();
}
$route->setNamespace($namespace);
}
$newPrefixes = $prefixes;
if($route->getPrefix() && trim($route->getPrefix(), '/') !== '') {
array_push($newPrefixes, trim($route->getPrefix(), '/'));
}
/* @var $group RouterGroup */
$group = null;
if(!($route instanceof RouterGroup)) {
if(is_array($newPrefixes) && count($newPrefixes) && $backStack) {
$route->setUrl( '/' . join('/', $newPrefixes) . $route->getUrl() );
}
$this->controllerUrlMap[] = $route;
}
$this->currentRoute = $route;
if($route instanceof RouterGroup && is_callable($route->getCallback())) {
$route->renderRoute($this->request);
if($route->matchRoute($this->request)) {
$group = $route;
$mergedSettings = array_merge($settings, $group->getMergeableSettings());
// Add ExceptionHandler
if ($group->getExceptionHandler() !== null) {
$this->exceptionHandlers[] = $route;
}
}
}
$this->currentRoute = null;
if(count($this->backStack)) {
$backStack = $this->backStack;
$this->backStack = array();
// Route any routes added to the backstack
$this->processRoutes($backStack, $mergedSettings, $newPrefixes, true, $group);
}
}
}
public function routeRequest($original = true) {
$originalUri = $this->request->getUri();
$routeNotAllowed = false;
try {
// Initialize boot-managers
if(count($this->bootManagers)) {
/* @var $manager RouterBootManager */
foreach($this->bootManagers as $manager) {
$this->request = $manager->boot($this->request);
if(!($this->request instanceof Request)) {
throw new RouterException('Custom router bootmanager "'. get_class($manager) .'" must return instance of Request.');
}
}
}
// Loop through each route-request
$this->processRoutes($this->routes);
if($original === true) {
// Verify csrf token for request
if ($this->baseCsrfVerifier !== null) {
$this->baseCsrfVerifier->handle($this->request);
}
}
$max = count($this->controllerUrlMap);
/* @var $route RouterEntry */
for ($i = 0; $i < $max; $i++) {
$route = $this->controllerUrlMap[$i];
$routeMatch = $route->matchRoute($this->request);
if ($routeMatch) {
if (count($route->getRequestMethods()) && !in_array($this->request->getMethod(), $route->getRequestMethods())) {
$routeNotAllowed = true;
continue;
}
$routeNotAllowed = false;
$this->request->rewrite_uri = $this->request->uri;
$this->request->setUri($originalUri);
$this->request->loadedRoute = $route;
$route->loadMiddleware($this->request);
$this->request->loadedRoute->renderRoute($this->request);
break;
}
}
} catch(\Exception $e) {
$this->handleException($e);
}
if($routeNotAllowed) {
$this->handleException(new RouterException('Route or method not allowed', 403));
}
if(!$this->request->loadedRoute) {
$this->handleException(new RouterException(sprintf('Route not found: %s', $this->request->getUri()), 404));
}
}
protected function handleException(\Exception $e) {
$request = null;
/* @var $route RouterGroup */
foreach ($this->exceptionHandlers as $route) {
$route->loadMiddleware($this->request);
$handler = $route->getExceptionHandler();
$handler = new $handler();
if (!($handler instanceof IExceptionHandler)) {
throw new RouterException('Exception handler must implement the IExceptionHandler interface.');
}
$request = $handler->handleError($this->request, $this->request->loadedRoute, $e);
}
if($request !== null) {
$this->request = $request;
$this->routeRequest(false);
return;
}
throw $e;
}
/**
* @return string
*/
public function getDefaultNamespace(){
return $this->defaultNamespace;
}
/**
* @param string $defaultNamespace
* @return static
*/
public function setDefaultNamespace($defaultNamespace) {
$this->defaultNamespace = $defaultNamespace;
return $this;
}
/**
* @return array
*/
public function getBootManagers() {
return $this->bootManagers;
}
/**
* @param array $bootManagers
*/
public function setBootManagers(array $bootManagers) {
$this->bootManagers = $bootManagers;
}
public function addBootManager(RouterBootManager $bootManager) {
$this->bootManagers[] = $bootManager;
}
/**
* @return RouterEntry
*/
public function getLoadedRoute() {
if(!($this->request->loadedRoute instanceof RouterGroup)) {
return $this->request->loadedRoute;
}
return null;
}
/**
* @return array
*/
public function getBackstack() {
return $this->backStack;
}
/**
* @return RouterEntry
*/
public function getCurrentRoute(){
return $this->currentRoute;
}
/**
* @return array
*/
public function getRoutes(){
return $this->routes;
}
/**
* Get current request
*
* @return Request
*/
public function getRequest() {
return $this->request;
}
/**
* Get base csrf verifier class
* @return BaseCsrfVerifier
*/
public function getBaseCsrfVerifier() {
return $this->baseCsrfVerifier;
}
/**
* Set base csrf verifier class
*
* @param BaseCsrfVerifier $baseCsrfVerifier
* @return self
*/
public function setBaseCsrfVerifier(BaseCsrfVerifier $baseCsrfVerifier) {
$this->baseCsrfVerifier = $baseCsrfVerifier;
return $this;
}
public function arrayToParams(array $getParams = null, $includeEmpty = true) {
if(is_array($getParams) && count($getParams)) {
if ($includeEmpty === false) {
$getParams = array_filter($getParams, function ($item) {
return (!empty($item));
});
}
return '?' . http_build_query($getParams);
}
return '';
}
protected function processUrl(RouterRoute $route, $method = null, $parameters = null, $getParams = null) {
$domain = '';
if($route->getGroup() !== null && $route->getGroup()->getDomain() !== null) {
if(is_array($route->getGroup()->getDomain())) {
$domains = $route->getGroup()->getDomain();
$domain = array_shift($domains);
} else {
$domain = $route->getGroup()->getDomain();
}
$domain = '//' . $domain;
}
$url = $domain . '/' . trim($route->getUrl(), '/');
if(($route instanceof RouterController || $route instanceof RouterResource) && $method !== null) {
$url .= $method;
}
if($route instanceof RouterController || $route instanceof RouterResource) {
if(count($parameters)) {
$url .= join('/', $parameters);
}
} else {
/* @var $route RouterEntry */
if(is_array($parameters)) {
$params = array_merge($route->getParameters(), $parameters);
} else {
$params = $route->getParameters();
}
$otherParams = [];
$i = 0;
foreach($params as $param => $value) {
$value = (isset($parameters[$param])) ? $parameters[$param] : $value;
if(stripos($url, '{' . $param. '}') !== false || stripos($url, '{' . $param . '?}') !== false) {
$url = str_ireplace(array('{' . $param . '}', '{' . $param . '?}'), $value, $url);
} else {
$otherParams[$param] = $value;
}
$i++;
}
$url = rtrim($url, '/') . '/' . join('/', $otherParams);
}
$url = rtrim($url, '/') . '/';
if($getParams !== null) {
$url .= $this->arrayToParams($getParams);
}
return $url;
}
public function getRoute($controller = null, $parameters = null, $getParams = null) {
if($parameters !== null && !is_array($parameters)) {
throw new \InvalidArgumentException('Invalid type for parameter. Must be array or null');
}
if($getParams !== null && !is_array($getParams)) {
throw new \InvalidArgumentException('Invalid type for getParams. Must be array or null');
}
// Return current route if no options has been specified
if($controller === null && $parameters === null) {
$getParams = (is_array($getParams)) ? array_merge($_GET, $getParams) : $_GET;
$url = parse_url($this->request->getUri(), PHP_URL_PATH);
if($getParams !== null) {
$url .= $this->arrayToParams($getParams);
}
return $url;
}
if($controller === null && $this->request->loadedRoute !== null) {
return $this->processUrl($this->request->loadedRoute, $this->request->loadedRoute->getMethod(), $parameters, $getParams);
}
$c = '';
$method = null;
$max = count($this->controllerUrlMap);
/* @var $route RouterRoute */
for($i = 0; $i < $max; $i++) {
$route = $this->controllerUrlMap[$i];
// Check an alias exist, if the matches - use it
if($route instanceof RouterRoute && $route->hasAlias($controller)) {
return $this->processUrl($route, $route->getMethod(), $parameters, $getParams);
}
if($route instanceof RouterRoute && !is_callable($route->getCallback()) && stripos($route->getCallback(), '@') !== false) {
$c = $route->getCallback();
} else if($route instanceof RouterController || $route instanceof RouterResource) {
$c = $route->getController();
}
if($c === $controller || strpos($c, $controller) === 0) {
return $this->processUrl($route, $route->getMethod(), $parameters, $getParams);
}
}
$c = '';
// No match has yet been found, let's try to guess what url that should be returned
for($i = 0; $i < $max; $i++) {
$route = $this->controllerUrlMap[$i];
if($route instanceof RouterRoute && !is_callable($route->getCallback()) && stripos($route->getCallback(), '@') !== false) {
$c = $route->getClass();
} else if($route instanceof RouterController || $route instanceof RouterResource) {
$c = $route->getController();
}
if(stripos($controller, '@') !== false) {
$tmp = explode('@', $controller);
$controller = $tmp[0];
$method = $tmp[1];
}
if($controller === $c) {
return $this->processUrl($route, $method, $parameters, $getParams);
}
}
$controller = ($controller === null) ? '/' : $controller;
$url = array($controller);
if(is_array($parameters)) {
foreach($parameters as $key => $value) {
array_push($url,$value);
}
}
$url = '/' . trim(join('/', $url), '/') . '/';
if($getParams !== null) {
$url .= $this->arrayToParams($getParams);
}
return $url;
}
public static function getInstance() {
if(static::$instance === null) {
static::$instance = new static();
}
return static::$instance;
}
}
@@ -0,0 +1,10 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Http\Request;
abstract class RouterBootManager {
abstract public function boot(Request $request);
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Exception\RouterException;
use Pecee\Http\Request;
class RouterController extends RouterEntry {
const DEFAULT_METHOD = 'index';
protected $url;
protected $controller;
protected $method;
public function __construct($url, $controller) {
parent::__construct();
$this->url = $url;
$this->controller = $controller;
}
public function renderRoute(Request $request) {
if(is_object($this->getCallback()) && is_callable($this->getCallback())) {
// When the callback is a function
call_user_func_array($this->getCallback(), $this->getParameters());
} else {
// When the callback is a method
$controller = explode('@', $this->getCallback());
$className = $this->getNamespace() . '\\' . $controller[0];
$class = $this->loadClass($className);
$method = $request->getMethod() . ucfirst($controller[1]);
if (!method_exists($class, $method)) {
throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404);
}
call_user_func_array(array($class, $method), $this->getParameters());
return $class;
}
return null;
}
public function matchRoute(Request $request) {
$url = parse_url(urldecode($request->getUri()));
$url = rtrim($url['path'], '/') . '/';
if(strtolower($url) == strtolower($this->url) || stripos($url, $this->url) === 0) {
$strippedUrl = trim(str_ireplace($this->url, '/', $url), '/');
$path = explode('/', $strippedUrl);
if(count($path)) {
$method = (!isset($path[0]) || trim($path[0]) === '') ? self::DEFAULT_METHOD : $path[0];
$this->method = $method;
array_shift($path);
$this->parameters = $path;
// Set callback
$this->setCallback($this->controller . '@' . $this->method);
return true;
}
}
return null;
}
/**
* @return string
*/
public function getUrl() {
return $this->url;
}
/**
* @param string $url
* @return static
*/
public function setUrl($url) {
$url = rtrim($url, '/') . '/';
$this->url = $url;
return $this;
}
/**
* @return string
*/
public function getController() {
return $this->controller;
}
/**
* @param string $controller
* @return static
*/
public function setController($controller) {
$this->controller = $controller;
return $this;
}
/**
* @return string
*/
public function getMethod() {
return $this->method;
}
/**
* @param string $method
* @return static
*/
public function setMethod($method) {
$this->method = $method;
return $this;
}
}
+420
View File
@@ -0,0 +1,420 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Exception\RouterException;
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
abstract class RouterEntry {
const REQUEST_TYPE_POST = 'post';
const REQUEST_TYPE_GET = 'get';
const REQUEST_TYPE_PUT = 'put';
const REQUEST_TYPE_PATCH = 'patch';
const REQUEST_TYPE_DELETE = 'delete';
public static $allowedRequestTypes = array(
self::REQUEST_TYPE_DELETE,
self::REQUEST_TYPE_GET,
self::REQUEST_TYPE_POST,
self::REQUEST_TYPE_PUT,
self::REQUEST_TYPE_PATCH
);
protected $settings;
protected $callback;
public function __construct() {
$this->settings = array();
$this->settings['requestMethods'] = array();
$this->settings['where'] = array();
$this->settings['parameters'] = array();
}
/**
* Returns callback name/identifier for the current route based on the callback.
* Useful if you need to get a unique identifier for the loaded route, for instance
* when using translations etc.
*
* @return string
*/
public function getIdentifier() {
if(strpos($this->callback, '@') !== false) {
return $this->callback;
}
return 'function_' . md5($this->callback);
}
/**
* @param string $callback
* @return self;
*/
public function setCallback($callback) {
$this->callback = $callback;
return $this;
}
/**
* @return mixed
*/
public function getCallback() {
return $this->callback;
}
public function getMethod() {
if(strpos($this->callback, '@') !== false) {
$tmp = explode('@', $this->callback);
return $tmp[1];
}
return null;
}
public function getClass() {
if(strpos($this->callback, '@') !== false) {
$tmp = explode('@', $this->callback);
return $tmp[0];
}
return null;
}
public function setMethod($method) {
$this->callback = sprintf('%s@%s', $this->getClass(), $method);
return $this;
}
public function setClass($class) {
$this->callback = sprintf('%s@%s', $class, $this->getMethod());
return $this;
}
/**
* @param string $prefix
* @return self
*/
public function setPrefix($prefix) {
$this->prefix = '/' . ltrim($prefix, '/');
return $this;
}
/**
* @param string $middleware
* @return self
*/
public function setMiddleware($middleware) {
$this->middleware = $middleware;
return $this;
}
/**
* @param string $namespace
* @return self
*/
public function setNamespace($namespace) {
$this->namespace = $namespace;
return $this;
}
/**
* @return string
*/
public function getPrefix() {
return $this->prefix;
}
/**
* @return string|array
*/
public function getMiddleware() {
return $this->middleware;
}
/**
* @return string
*/
public function getNamespace() {
return $this->namespace;
}
/**
* @return array
*/
public function getSettings() {
return $this->settings;
}
/**
* @return mixed
*/
public function getParameters(){
return ($this->parameters === null) ? array() : $this->parameters;
}
/**
* @param mixed $parameters
* @return self
*/
public function setParameters($parameters) {
$this->parameters = $parameters;
return $this;
}
/**
* Add regular expression parameter match
*
* @param array $options
* @return self
*/
public function where(array $options) {
$this->where = array_merge($this->where, $options);
return $this;
}
/**
* Add regular expression match for url
*
* @param string $regex
* @return self
*/
public function match($regex) {
$this->regexMatch = $regex;
return $this;
}
/**
* Get settings that are allowed to be inherited by child routes.
*
* @return array
*/
public function getMergeableSettings() {
$settings = $this->settings;
if(isset($settings['prefix'])) {
unset($settings['prefix']);
}
return $settings;
}
/**
* @param array $settings
* @return self
*/
public function addSettings(array $settings = null) {
if(is_array($settings)) {
$this->settings = array_merge($this->settings, $settings);
}
return $this;
}
/**
* @param array $settings
* @return self
*/
public function setSettings($settings) {
$this->settings = $settings;
if(isset($settings['prefix'])) {
$this->setPrefix($settings['prefix']);
}
return $this;
}
/**
* Dynamically access settings value
*
* @param $name
* @return mixed|null
*/
public function __get($name) {
return (isset($this->settings[$name]) ? $this->settings[$name] : null);
}
/**
* Dynamically set settings value
*
* @param string $name
* @param mixed|null $value
*/
public function __set($name, $value = null) {
$this->settings[$name] = $value;
}
protected function loadClass($name) {
if(!class_exists($name)) {
throw new RouterException(sprintf('Class %s does not exist', $name));
}
return new $name();
}
protected function parseParameters($route, $url, $parameterRegex = '[\w]+') {
$parameterNames = array();
$regex = '';
$lastCharacter = '';
$isParameter = false;
$parameter = '';
$routeLength = strlen($route);
for($i = 0; $i < $routeLength; $i++) {
$character = $route[$i];
if($character === '{') {
// Remove "/" and "\" from regex
if(substr($regex, strlen($regex)-1) === '/') {
$regex = substr($regex, 0, strlen($regex) - 2);
}
$isParameter = true;
} elseif($isParameter && $character === '}') {
$required = true;
// Check for optional parameter
// Use custom parameter regex if it exists
if(is_array($this->where) && isset($this->where[$parameter])) {
$parameterRegex = $this->where[$parameter];
}
if($lastCharacter === '?') {
$parameter = substr($parameter, 0, strlen($parameter)-1);
$regex .= '(?:\/?(?P<' . $parameter . '>'. $parameterRegex .')[^\/]?)?';
$required = false;
} else {
$regex .= '\/?(?P<' . $parameter . '>'. $parameterRegex .')[^\/]?';
}
$parameterNames[] = array('name' => $parameter, 'required' => $required);
$parameter = '';
$isParameter = false;
} elseif($isParameter) {
$parameter .= $character;
} elseif($character === '/') {
$regex .= '\\' . $character;
} else {
$regex .= str_replace('.', '\\.', $character);
}
$lastCharacter = $character;
}
$parameterValues = array();
if(preg_match('/^'.$regex.'\/?$/is', $url, $parameterValues)) {
$parameters = array();
$max = count($parameterNames);
if($max) {
for($i = 0; $i < $max; $i++) {
$name = $parameterNames[$i];
$parameterValue = isset($parameterValues[$name['name']]) ? $parameterValues[$name['name']] : null;
if($name['required'] && $parameterValue === null) {
throw new RouterException('Missing required parameter ' . $name['name'], 404);
}
if(!$name['required'] && $parameterValue === null) {
continue;
}
$parameters[$name['name']] = $parameterValue;
}
}
return $parameters;
}
return null;
}
public function loadMiddleware(Request $request) {
if($this->getMiddleware()) {
if(is_array($this->getMiddleware())) {
foreach($this->getMiddleware() as $middleware) {
$middleware = $this->loadClass($middleware);
if (!($middleware instanceof IMiddleware)) {
throw new RouterException($middleware . ' must be instance of Middleware');
}
/* @var $class IMiddleware */
$middleware->handle($request);
}
} else {
$middleware = $this->loadClass($this->getMiddleware());
if (!($middleware instanceof IMiddleware)) {
throw new RouterException($this->getMiddleware() . ' must be instance of Middleware');
}
/* @var $class IMiddleware */
$middleware->handle($request);
}
}
}
public function renderRoute(Request $request) {
if(is_object($this->getCallback()) && is_callable($this->getCallback())) {
// When the callback is a function
call_user_func_array($this->getCallback(), $this->getParameters());
} else {
// When the callback is a method
$controller = explode('@', $this->getCallback());
$className = $this->getNamespace() . '\\' . $controller[0];
$class = $this->loadClass($className);
$method = $controller[1];
if (!method_exists($class, $method)) {
throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404);
}
$parameters = array_filter($this->getParameters(), function($var){
return !is_null($var);
});
call_user_func_array(array($class, $method), $parameters);
return $class;
}
return null;
}
/**
* Set allowed request methods
*
* @param array $methods
* @return self $this
*/
public function setRequestMethods(array $methods) {
$this->settings['requestMethods'] = $methods;
return $this;
}
/**
* Get allowed request methods
*
* @return array
*/
public function getRequestMethods() {
if(!isset($this->settings['requestMethods']) || isset($this->settings['requestMethods']) && !is_array($this->settings['requestMethods'])) {
$value = isset($this->settings['requestMethods']) ? $this->settings['requestMethods'] : null;
return array($value);
}
return $this->settings['requestMethods'];
}
public function getGroup() {
return $this->group;
}
public function setGroup($group) {
$this->group = $group;
return $this;
}
abstract function matchRoute(Request $request);
}
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Exception\RouterException;
use Pecee\Http\Request;
class RouterGroup extends RouterEntry {
public function matchDomain(Request $request) {
if($this->domain !== null) {
if(is_array($this->domain)) {
$max = count($this->domain);
for($i = 0; $i < $max; $i++) {
$domain = $this->domain[$i];
$parameters = $this->parseParameters($domain, $request->getHost(), '[^.]*');
if($parameters !== null) {
$this->parameters = $parameters;
return true;
}
}
return false;
}
$parameters = $this->parseParameters($this->domain, $request->getHost(), '[^.]*');
if ($parameters !== null) {
$this->parameters = $parameters;
return true;
}
return false;
}
return true;
}
public function renderRoute(Request $request) {
// Check if request method is allowed
$hasAccess = (!$this->method);
if($this->method) {
if(is_array($this->method)) {
$hasAccess = (in_array($request->getMethod(), $this->getRequestMethods()));
} else {
$hasAccess = strtolower($this->getRequestMethods()) == strtolower($request->getMethod());
}
}
if(!$hasAccess) {
throw new RouterException('Method not allowed');
}
$this->matchDomain($request);
return parent::renderRoute($request);
}
public function matchRoute(Request $request) {
// Skip if prefix doesn't match
if($this->getPrefix() !== null && stripos($request->getUri(), $this->getPrefix()) === false) {
return false;
}
return $this->matchDomain($request);
}
public function setExceptionHandler($class) {
$this->exceptionHandler = $class;
return $this;
}
public function getExceptionHandler() {
return $this->exceptionHandler;
}
public function getDomain() {
return $this->domain;
}
/**
* @param array $settings
* @return self
*/
public function addSettings(array $settings = null) {
if($this->getNamespace() !== null && isset($settings['namespace'])) {
unset($settings['namespace']);
}
// Push middleware if multiple
if($this->getMiddleware() !== null && isset($settings['middleware'])) {
if(!is_array($this->getMiddleware())) {
$middlewares = [
$this->getMiddleware(),
$settings['middleware']
];
} else {
$middlewares = array_push($settings['middleware'], $this->getMiddleware());
}
$settings['middleware'] = array_unique(array_reverse($middlewares));
}
if(is_array($settings)) {
$this->settings = array_merge($this->settings, $settings);
}
return $this;
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Exception\RouterException;
use Pecee\Http\Request;
class RouterResource extends RouterEntry {
protected $url;
protected $controller;
protected $postMethod;
public function __construct($url, $controller) {
parent::__construct();
$this->url = $url;
$this->controller = $controller;
$this->postMethod = strtolower(($_SERVER['REQUEST_METHOD'] != 'GET') ? 'post' : 'get');
}
public function renderRoute(Request $request) {
if(is_object($this->getCallback()) && is_callable($this->getCallback())) {
// When the callback is a function
call_user_func_array($this->getCallback(), $this->getParameters());
} else {
// When the callback is a method
$controller = explode('@', $this->getCallback());
$className = $this->getNamespace() . '\\' . $controller[0];
$class = $this->loadClass($className);
$method = strtolower($controller[1]);
if (!method_exists($class, $method)) {
throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404);
}
call_user_func_array(array($class, $method), $this->getParameters());
return $class;
}
return null;
}
protected function call($method, $parameters) {
$this->setCallback($this->controller . '@' . $method);
$this->parameters = $parameters;
return true;
}
public function matchRoute(Request $request) {
$url = parse_url(urldecode($request->getUri()));
$url = rtrim($url['path'], '/') . '/';
$route = rtrim($this->url, '/') . '/{id?}/{action?}';
$parameters = $this->parseParameters($route, $url);
if($parameters !== null) {
if(is_array($parameters)) {
$parameters = array_merge($this->parameters, $parameters);
}
$action = isset($parameters['action']) ? $parameters['action'] : null;
unset($parameters['action']);
// Delete
if($request->getMethod() === self::REQUEST_TYPE_DELETE && $this->postMethod === self::REQUEST_TYPE_POST) {
return $this->call('destroy', $parameters);
}
// Update
if(in_array($request->getMethod(), array(self::REQUEST_TYPE_PATCH, self::REQUEST_TYPE_PUT)) && $this->postMethod === self::REQUEST_TYPE_POST) {
return $this->call('update', $parameters);
}
// Edit
if(isset($action) && strtolower($action) === 'edit' && $this->postMethod === self::REQUEST_TYPE_GET) {
return $this->call('edit', $parameters);
}
// Create
if(strtolower($action) === 'create' && $request->getMethod() === self::REQUEST_TYPE_GET) {
return $this->call('create', $parameters);
}
// Save
if($this->postMethod === self::REQUEST_TYPE_POST) {
return $this->call('store', $parameters);
}
// Show
if(isset($parameters['id']) && $this->postMethod === self::REQUEST_TYPE_GET) {
return $this->call('show', $parameters);
}
// Index
return $this->call('index', $parameters);
}
return null;
}
/**
* @return string
*/
public function getUrl() {
return $this->url;
}
/**
* @param string $url
* @return static
*/
public function setUrl($url) {
$url = rtrim($url, '/') . '/';
$this->url = $url;
return $this;
}
/**
* @return string
*/
public function getController() {
return $this->controller;
}
/**
* @param string $controller
* @return static
*/
public function setController($controller) {
$this->controller = $controller;
return $this;
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\Http\Request;
class RouterRoute extends RouterEntry {
const PARAMETERS_REGEX_MATCH = '{([A-Za-z\-\_]*?)\?{0,1}}';
protected $url;
public function __construct($url, $callback) {
parent::__construct();
$this->setUrl($url);
$this->setCallback($callback);
}
public function matchRoute(Request $request) {
$url = parse_url(urldecode($request->getUri()));
$url = rtrim($url['path'], '/') . '/';
// Match on custom defined regular expression
if($this->regexMatch) {
$parameters = array();
if(preg_match('/('.$this->regexMatch.')/is', $request->getHost() . $url, $parameters)) {
$this->parameters = (!is_array($parameters[0]) ? array($parameters[0]) : $parameters[0]);
return true;
}
return null;
}
// Make regular expression based on route
$route = rtrim($this->url, '/') . '/';
$parameters = $this->parseParameters($route, $url);
if($parameters !== null) {
if(is_array($this->parameters)) {
$this->parameters = array_merge($this->parameters, $parameters);
} else {
$this->parameters = $parameters;
}
return true;
}
return null;
}
/**
* @return string
*/
public function getUrl() {
return $this->url;
}
/**
* @param string $url
* @return self
*/
public function setUrl($url) {
$parameters = array();
$matches = array();
if(preg_match_all('/'.self::PARAMETERS_REGEX_MATCH.'/is', $url, $matches)) {
$parameters = $matches[1];
}
if(count($parameters)) {
$tmp = array();
foreach($parameters as $param) {
$tmp[$param] = null;
}
$this->parameters = $tmp;
}
$this->url = $url;
return $this;
}
/**
* Get alias for the url which can be used when getting the url route.
* @return string|array
*/
public function getAlias(){
return $this->alias;
}
/**
* Check if route has given alias.
*
* @param $name
* @return bool
*/
public function hasAlias($name) {
if(is_array($this->alias)) {
foreach($this->alias as $alias) {
if(strtolower($alias) === strtolower($name)) {
return true;
}
}
} else {
return strtolower($this->getAlias()) === strtolower($name);
}
return false;
}
/**
* Set the url alias for easier getting the url route.
* @param string|array $alias
* @return self
*/
public function setAlias($alias){
$this->alias = $alias;
return $this;
}
public function setSettings($settings) {
// Change as to alias
if(isset($settings{'as'})) {
$this->setAlias($settings['as']);
}
return parent::setSettings($settings);
}
}
+73 -479
View File
@@ -1,537 +1,131 @@
<?php
/**
* ---------------------------
* Router helper class
* ---------------------------
*
* This class is added so calls can be made statically like SimpleRouter::get() making the code look pretty.
* It also adds some extra functionality like default-namespace etc.
* This class is added so calls can be made statically like Router::get() making the code look more pretty.
*/
namespace Pecee\SimpleRouter;
use Closure;
use Exception;
use Pecee\Exceptions\InvalidArgumentException;
use Pecee\Http\Middleware\BaseCsrfVerifier;
use Pecee\Http\Request;
use Pecee\Http\Response;
use Pecee\Http\Url;
use Pecee\SimpleRouter\ClassLoader\IClassLoader;
use Pecee\SimpleRouter\Exceptions\HttpException;
use Pecee\SimpleRouter\Handlers\CallbackExceptionHandler;
use Pecee\SimpleRouter\Handlers\IEventHandler;
use Pecee\SimpleRouter\Route\IGroupRoute;
use Pecee\SimpleRouter\Route\ILoadableRoute;
use Pecee\SimpleRouter\Route\IPartialGroupRoute;
use Pecee\SimpleRouter\Route\IRoute;
use Pecee\SimpleRouter\Route\RouteController;
use Pecee\SimpleRouter\Route\RouteGroup;
use Pecee\SimpleRouter\Route\RoutePartialGroup;
use Pecee\SimpleRouter\Route\RouteResource;
use Pecee\SimpleRouter\Route\RouteUrl;
class SimpleRouter
{
/**
* Default namespace added to all routes
* @var string|null
*/
protected static $defaultNamespace;
class SimpleRouter {
/**
* The response object
* @var Response
* Start/route request
* @param null $defaultNamespace
* @throws \Pecee\Exception\RouterException
*/
protected static $response;
/**
* Router instance
* @var Router
*/
protected static $router;
/**
* Start routing
*
* @throws \Pecee\SimpleRouter\Exceptions\NotFoundHttpException
* @throws \Pecee\Http\Middleware\Exceptions\TokenMismatchException
* @throws HttpException
* @throws Exception
*/
public static function start(): void
{
// Set default namespaces
foreach (static::router()->getRoutes() as $route) {
static::addDefaultNamespace($route);
}
echo static::router()->start();
public static function start($defaultNamespace = null) {
RouterBase::getInstance()->setDefaultNamespace($defaultNamespace)->routeRequest();
}
/**
* Start the routing an return array with debugging-information
*
* @return array
*/
public static function startDebug(): array
{
$routerOutput = null;
try {
ob_start();
static::router()->setDebugEnabled(true)->start();
$routerOutput = ob_get_clean();
} catch (Exception $e) {
}
// Try to parse library version
$composerFile = dirname(__DIR__, 3) . '/composer.lock';
$version = false;
if (is_file($composerFile) === true) {
$composerInfo = json_decode(file_get_contents($composerFile), true);
if (isset($composerInfo['packages']) === true && is_array($composerInfo['packages']) === true) {
foreach ($composerInfo['packages'] as $package) {
if (isset($package['name']) === true && strtolower($package['name']) === 'pecee/simple-router') {
$version = $package['version'];
break;
}
}
}
}
$request = static::request();
$router = static::router();
return [
'url' => $request->getUrl(),
'method' => $request->getMethod(),
'host' => $request->getHost(),
'loaded_routes' => $request->getLoadedRoutes(),
'all_routes' => $router->getRoutes(),
'boot_managers' => $router->getBootManagers(),
'csrf_verifier' => $router->getCsrfVerifier(),
'log' => $router->getDebugLog(),
'event_handlers' => $router->getEventHandlers(),
'router_output' => $routerOutput,
'library_version' => $version,
'php_version' => PHP_VERSION,
'server_params' => $request->getHeaders(),
];
}
/**
* Set default namespace which will be prepended to all routes.
*
* @param string $defaultNamespace
*/
public static function setDefaultNamespace(string $defaultNamespace): void
{
static::$defaultNamespace = $defaultNamespace;
}
/**
* Base CSRF verifier
*
* Set base csrf verifier
* @param BaseCsrfVerifier $baseCsrfVerifier
*/
public static function csrfVerifier(BaseCsrfVerifier $baseCsrfVerifier): void
{
static::router()->setCsrfVerifier($baseCsrfVerifier);
public static function csrfVerifier(BaseCsrfVerifier $baseCsrfVerifier) {
RouterBase::getInstance()->setBaseCsrfVerifier($baseCsrfVerifier);
}
/**
* Add new event handler to the router
*
* @param IEventHandler $eventHandler
*/
public static function addEventHandler(IEventHandler $eventHandler): void
{
static::router()->addEventHandler($eventHandler);
public static function addBootManager(RouterBootManager $bootManager) {
RouterBase::getInstance()->addBootManager($bootManager);
}
/**
* Boot managers allows you to alter the routes before the routing occurs.
* Perfect if you want to load pretty-urls from a file or database.
*
* @param IRouterBootManager $bootManager
*/
public static function addBootManager(IRouterBootManager $bootManager): void
{
static::router()->addBootManager($bootManager);
public static function get($url, $callback, array $settings = null) {
return self::match(['get'], $url, $callback, $settings);
}
/**
* Redirect to when route matches.
*
* @param string $where
* @param string $to
* @param int $httpCode
* @return IRoute
*/
public static function redirect(string $where, string $to, int $httpCode = 301): IRoute
{
return static::get($where, static function () use ($to, $httpCode): void {
static::response()->redirect($to, $httpCode);
});
public static function post($url, $callback, array $settings = null) {
return self::match(['post'], $url, $callback, $settings);
}
/**
* Route the given url to your callback on GET request method.
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
*
* @return RouteUrl|IRoute
*/
public static function get(string $url, $callback, array $settings = null): IRoute
{
return static::match([Request::REQUEST_TYPE_GET], $url, $callback, $settings);
public static function put($url, $callback, array $settings = null) {
return self::match(['put'], $url, $callback, $settings);
}
/**
* Route the given url to your callback on POST request method.
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
*/
public static function post(string $url, $callback, array $settings = null): IRoute
{
return static::match([Request::REQUEST_TYPE_POST], $url, $callback, $settings);
public static function delete($url, $callback, array $settings = null) {
return self::match(['delete'], $url, $callback, $settings);
}
/**
* Route the given url to your callback on PUT request method.
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
*/
public static function put(string $url, $callback, array $settings = null): IRoute
{
return static::match([Request::REQUEST_TYPE_PUT], $url, $callback, $settings);
}
/**
* Route the given url to your callback on PATCH request method.
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
*/
public static function patch(string $url, $callback, array $settings = null): IRoute
{
return static::match([Request::REQUEST_TYPE_PATCH], $url, $callback, $settings);
}
/**
* Route the given url to your callback on OPTIONS request method.
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
*/
public static function options(string $url, $callback, array $settings = null): IRoute
{
return static::match([Request::REQUEST_TYPE_OPTIONS], $url, $callback, $settings);
}
/**
* Route the given url to your callback on DELETE request method.
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
*/
public static function delete(string $url, $callback, array $settings = null): IRoute
{
return static::match([Request::REQUEST_TYPE_DELETE], $url, $callback, $settings);
}
/**
* Groups allows for encapsulating routes with special settings.
*
* @param array $settings
* @param Closure $callback
* @return RouteGroup|IGroupRoute
* @throws InvalidArgumentException
*/
public static function group(array $settings, Closure $callback): IGroupRoute
{
$group = new RouteGroup();
public static function group($settings = array(), $callback) {
$group = new RouterGroup();
$group->setCallback($callback);
$group->setSettings($settings);
static::router()->addRoute($group);
if($settings !== null && is_array($settings)) {
$group->setSettings($settings);
}
RouterBase::getInstance()->addRoute($group);
return $group;
}
/**
* Special group that has the same benefits as group but supports
* parameters and which are only rendered when the url matches.
* Adds get + post route
*
* @param string $url
* @param Closure $callback
* @param array $settings
* @return RoutePartialGroup|IPartialGroupRoute
* @throws InvalidArgumentException
* @param callable $callback
* @param array|null $settings
* @return RouterRoute
*/
public static function partialGroup(string $url, Closure $callback, array $settings = []): IPartialGroupRoute
{
$settings['prefix'] = $url;
$group = new RoutePartialGroup();
$group->setSettings($settings);
$group->setCallback($callback);
static::router()->addRoute($group);
return $group;
public static function basic($url, $callback, array $settings = null) {
return self::match(['get', 'post'], $url, $callback, $settings);
}
/**
* Alias for the form method
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
* @see SimpleRouter::form
*/
public static function basic(string $url, $callback, array $settings = null): IRoute
{
return static::form($url, $callback, $settings);
}
/**
* This type will route the given url to your callback on the provided request methods.
* Route the given url to your callback on POST and GET request method.
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
* @see SimpleRouter::form
*/
public static function form(string $url, $callback, array $settings = null): IRoute
{
return static::match([
Request::REQUEST_TYPE_GET,
Request::REQUEST_TYPE_POST,
], $url, $callback, $settings);
}
/**
* This type will route the given url to your callback on the provided request methods.
*
* @param array $requestMethods
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
*/
public static function match(array $requestMethods, string $url, $callback, array $settings = null): IRoute
{
$route = new RouteUrl($url, $callback);
public static function match(array $requestMethods, $url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
$route->setRequestMethods($requestMethods);
if ($settings !== null) {
$route->setSettings($settings);
if($settings !== null) {
$route->addSettings($settings);
}
return static::router()->addRoute($route);
}
/**
* This type will route the given url to your callback and allow any type of request method
*
* @param string $url
* @param string|array|Closure $callback
* @param array|null $settings
* @return RouteUrl|IRoute
*/
public static function all(string $url, $callback, array $settings = null): IRoute
{
$route = new RouteUrl($url, $callback);
if ($settings !== null) {
$route->setSettings($settings);
}
return static::router()->addRoute($route);
}
/**
* This route will route request from the given url to the controller.
*
* @param string $url
* @param string $controller
* @param array|null $settings
* @return RouteController|IRoute
*/
public static function controller(string $url, string $controller, array $settings = null): IRoute
{
$route = new RouteController($url, $controller);
if ($settings !== null) {
$route->setSettings($settings);
}
return static::router()->addRoute($route);
}
/**
* This type will route all REST-supported requests to different methods in the provided controller.
*
* @param string $url
* @param string $controller
* @param array|null $settings
* @return RouteResource|IRoute
*/
public static function resource(string $url, string $controller, array $settings = null): IRoute
{
$route = new RouteResource($url, $controller);
if ($settings !== null) {
$route->setSettings($settings);
}
return static::router()->addRoute($route);
}
/**
* Add exception callback handler.
*
* @param Closure $callback
* @return CallbackExceptionHandler $callbackHandler
*/
public static function error(Closure $callback): CallbackExceptionHandler
{
$callbackHandler = new CallbackExceptionHandler($callback);
static::router()->addExceptionHandler($callbackHandler);
return $callbackHandler;
}
/**
* Get url for a route by using either name/alias, class or method name.
*
* The name parameter supports the following values:
* - Route name
* - Controller/resource name (with or without method)
* - Controller class name
*
* When searching for controller/resource by name, you can use this syntax "route.name@method".
* You can also use the same syntax when searching for a specific controller-class "MyController@home".
* If no arguments is specified, it will return the url for the current loaded route.
*
* @param string|null $name
* @param string|array|null $parameters
* @param array|null $getParams
* @return Url
*/
public static function getUrl(?string $name = null, $parameters = null, ?array $getParams = null): Url
{
try {
return static::router()->getUrl($name, $parameters, $getParams);
} catch (Exception $e) {
return new Url('/');
}
}
/**
* Get the request
*
* @return Request
*/
public static function request(): Request
{
return static::router()->getRequest();
}
/**
* Get the response object
*
* @return Response
*/
public static function response(): Response
{
if (static::$response === null) {
static::$response = new Response(static::request());
}
return static::$response;
}
/**
* Returns the router instance
*
* @return Router
*/
public static function router(): Router
{
if (static::$router === null) {
static::$router = new Router();
}
return static::$router;
}
/**
* Prepends the default namespace to all new routes added.
*
* @param ILoadableRoute|IRoute $route
* @return IRoute
*/
public static function addDefaultNamespace(IRoute $route): IRoute
{
if (static::$defaultNamespace !== null) {
$route->setNamespace(static::$defaultNamespace);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
/**
* Changes the rendering behavior of the router.
* When enabled the router will render all routes that matches.
* When disabled the router will stop rendering at the first route that matches.
*
* @param bool $bool
*/
public static function enableMultiRouteRendering(bool $bool): void
{
static::router()->setRenderMultipleRoutes($bool);
public static function all($url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
if($settings !== null) {
$route->addSettings($settings);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
/**
* Set custom class-loader class used.
* @param IClassLoader $classLoader
*/
public static function setCustomClassLoader(IClassLoader $classLoader): void
{
static::router()->setClassLoader($classLoader);
public static function controller($url, $controller, array $settings = null) {
$route = new RouterController($url, $controller);
if($settings !== null) {
$route->addSettings($settings);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
/**
* Get default namespace
* @return string|null
*/
public static function getDefaultNamespace(): ?string
{
return static::$defaultNamespace;
public static function resource($url, $controller, array $settings = null) {
$route = new RouterResource($url, $controller);
if($settings !== null) {
$route->addSettings($settings);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
public static function getRoute($controller = null, $parameters = null, $getParams = null) {
return RouterBase::getInstance()->getRoute($controller, $parameters, $getParams);
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
class DummyController {
public function start() {
echo static::class . '@' .'start() OK';
}
public function param($params = null) {
$params = func_get_args();
echo 'Params: ' . join(', ', $params);
}
public function notFound() {
echo 'not found';
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
require_once 'Exceptions/MiddlewareLoadedException.php';
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
class DummyMiddleware implements IMiddleware {
public function handle(Request $request) {
throw new MiddlewareLoadedException('Middleware loaded!');
}
}
@@ -0,0 +1,2 @@
<?php
class MiddlewareLoadedException extends \Exception {}
+8
View File
@@ -0,0 +1,8 @@
<?php
class ExceptionHandler implements \Pecee\Handler\IExceptionHandler {
public function handleError(\Pecee\Http\Request $request, \Pecee\SimpleRouter\RouterEntry $router = null, \Exception $error){
throw $error;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
class GroupTest extends PHPUnit_Framework_TestCase {
protected $result;
protected function group() {
$this->result = true;
}
public function testGroup() {
$this->result = false;
\Pecee\SimpleRouter\SimpleRouter::group(['prefix' => '/group'], $this->group());
try {
\Pecee\SimpleRouter\SimpleRouter::start();
} catch(Exception $e) {
}
$this->assertTrue($this->result);
}
public function testNestedGroup() {
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setUri('/api/v1/test');
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setMethod('get');
\Pecee\SimpleRouter\SimpleRouter::group(['prefix' => '/api'], function() {
\Pecee\SimpleRouter\SimpleRouter::group(['prefix' => '/v1'], function() {
\Pecee\SimpleRouter\SimpleRouter::get('/test', 'DummyController@start');
});
});
\Pecee\SimpleRouter\SimpleRouter::start();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
require_once 'Dummy/Handler/ExceptionHandler.php';
class MiddlewareTest extends PHPUnit_Framework_TestCase {
public function testMiddlewareFound() {
\Pecee\Http\Request::getInstance()->setMethod('get');
\Pecee\Http\Request::getInstance()->setUri('/my/test/url');
\Pecee\SimpleRouter\SimpleRouter::group(['exceptionHandler' => 'ExceptionHandler'], function() {
\Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start', ['middleware' => 'DummyMiddleware']);
});
$found = false;
try {
\Pecee\SimpleRouter\SimpleRouter::start();
}catch(\Exception $e) {
$found = ($e instanceof MiddlewareLoadedException);
}
$this->assertTrue($found);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
require_once 'Dummy/Handler/ExceptionHandler.php';
class RouterRouteTest extends PHPUnit_Framework_TestCase {
public function testNotFound() {
\Pecee\Http\Request::getInstance()->setMethod('get');
\Pecee\Http\Request::getInstance()->setUri('/test-param1-param2');
\Pecee\SimpleRouter\SimpleRouter::group(['exceptionHandler' => 'ExceptionHandler'], function() {
\Pecee\SimpleRouter\SimpleRouter::get('/non-existing-path', 'DummyController@start');
});
$found = false;
try {
\Pecee\SimpleRouter\SimpleRouter::start();
}catch(\Exception $e) {
$found = ($e instanceof \Pecee\Exception\RouterException && $e->getCode() == 404);
}
$this->assertTrue($found);
}
public function testGet() {
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setUri('/my/test/url');
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setMethod('get');
\Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testPost() {
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setUri('/my/test/url');
\Pecee\Http\Request::getInstance()->setMethod('post');
\Pecee\SimpleRouter\SimpleRouter::post('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testPut() {
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setUri('/my/test/url');
\Pecee\Http\Request::getInstance()->setMethod('put');
\Pecee\SimpleRouter\SimpleRouter::put('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testDelete() {
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setUri('/my/test/url');
\Pecee\Http\Request::getInstance()->setMethod('delete');
\Pecee\SimpleRouter\SimpleRouter::delete('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testMethodNotAllowed() {
\Pecee\SimpleRouter\RouterBase::getInstance()->getRequest()->setUri('/my/test/url');
\Pecee\Http\Request::getInstance()->setMethod('post');
\Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start');
try {
\Pecee\SimpleRouter\SimpleRouter::start();
} catch(\Exception $e) {
$this->assertEquals(403, $e->getCode());
}
}
public function testSimpleParam() {
\Pecee\Http\Request::getInstance()->setMethod('get');
\Pecee\Http\Request::getInstance()->setUri('/test-param1');
\Pecee\SimpleRouter\SimpleRouter::get('/test-{param1}', 'DummyController@param');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testMultiParam() {
\Pecee\Http\Request::getInstance()->setMethod('get');
\Pecee\Http\Request::getInstance()->setUri('/test-param1-param2');
\Pecee\SimpleRouter\SimpleRouter::get('/test-{param1}-{param2}', 'DummyController@param');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testPathParam() {
\Pecee\Http\Request::getInstance()->setMethod('get');
\Pecee\Http\Request::getInstance()->setUri('/test/path/param1');
\Pecee\SimpleRouter\SimpleRouter::get('/test/path/{param}', 'DummyController@param');
\Pecee\SimpleRouter\SimpleRouter::start();
}
}
@@ -1,49 +0,0 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
require_once 'Dummy/Handler/ExceptionHandler.php';
require_once 'Dummy/Managers/TestBootManager.php';
require_once 'Dummy/Managers/FindUrlBootManager.php';
class BootManagerTest extends \PHPUnit\Framework\TestCase
{
public function testBootManagerRoutes()
{
$result = false;
TestRouter::get('/', function () use (&$result) {
$result = true;
});
TestRouter::get('/about', 'DummyController@method2');
TestRouter::get('/contact', 'DummyController@method3');
// Add boot-manager
TestRouter::addBootManager(new TestBootManager([
'/con' => '/about',
'/contact' => '/',
]));
TestRouter::debug('/contact');
$this->assertTrue($result);
}
public function testFindUrlFromBootManager()
{
TestRouter::get('/', 'DummyController@method1');
TestRouter::get('/about', 'DummyController@method2')->name('about');
TestRouter::get('/contact', 'DummyController@method3')->name('contact');
$result = false;
// Add boot-manager
TestRouter::addBootManager(new FindUrlBootManager($result));
TestRouter::debug('/');
$this->assertTrue($result);
}
}
@@ -1,30 +0,0 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
require_once 'Dummy/ClassLoader/CustomClassLoader.php';
class ClassLoaderTest extends \PHPUnit\Framework\TestCase
{
public function testCustomClassLoader()
{
$result = false;
TestRouter::setCustomClassLoader(new CustomClassLoader());
TestRouter::get('/', 'NonExistingClass@method3');
TestRouter::get('/test-closure', function($status) use(&$result) {
$result = $status;
});
$classLoaderClass = TestRouter::debugOutput('/', 'get', false);
TestRouter::debugOutput('/test-closure');
$this->assertEquals('method3', $classLoaderClass);
$this->assertTrue($result);
TestRouter::router()->reset();
}
}
@@ -1,66 +0,0 @@
<?php
require_once 'Dummy/CsrfVerifier/DummyCsrfVerifier.php';
require_once 'Dummy/Security/SilentTokenProvider.php';
class CsrfVerifierTest extends \PHPUnit\Framework\TestCase
{
public function testTokenPass()
{
global $_POST;
$tokenProvider = new SilentTokenProvider();
$_POST[DummyCsrfVerifier::POST_KEY] = $tokenProvider->getToken();
TestRouter::router()->reset();
$router = TestRouter::router();
$router->getRequest()->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST);
$router->getRequest()->setUrl(new \Pecee\Http\Url('/page'));
$csrf = new DummyCsrfVerifier();
$csrf->setTokenProvider($tokenProvider);
$csrf->handle($router->getRequest());
// If handle doesn't throw exception, the test has passed
$this->assertTrue(true);
}
public function testTokenFail()
{
$this->expectException(\Pecee\Http\Middleware\Exceptions\TokenMismatchException::class);
global $_POST;
$tokenProvider = new SilentTokenProvider();
$router = TestRouter::router();
$router->getRequest()->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST);
$router->getRequest()->setUrl(new \Pecee\Http\Url('/page'));
$csrf = new DummyCsrfVerifier();
$csrf->setTokenProvider($tokenProvider);
$csrf->handle($router->getRequest());
}
public function testExcludeInclude()
{
$router = TestRouter::router();
$csrf = new DummyCsrfVerifier();
$request = $router->getRequest();
$request->setUrl(new \Pecee\Http\Url('/exclude-page'));
$this->assertTrue($csrf->testSkip($router->getRequest()));
$request->setUrl(new \Pecee\Http\Url('/exclude-all/page'));
$this->assertTrue($csrf->testSkip($router->getRequest()));
$request->setUrl(new \Pecee\Http\Url('/exclude-all/include-page'));
$this->assertFalse($csrf->testSkip($router->getRequest()));
$request->setUrl(new \Pecee\Http\Url('/include-page'));
$this->assertFalse($csrf->testSkip($router->getRequest()));
}
}
@@ -1,71 +0,0 @@
<?php
require_once 'Dummy/DummyController.php';
require_once 'Dummy/Middleware/IpRestrictMiddleware.php';
class CustomMiddlewareTest extends \PHPUnit\Framework\TestCase
{
public function testIpBlock() {
$this->expectException(\Pecee\SimpleRouter\Exceptions\HttpException::class);
global $_SERVER;
// Test exact ip
$_SERVER['remote-addr'] = '5.5.5.5';
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/fail', 'DummyController@method1');
});
TestRouter::debug('/fail');
// Test ip-range
$_SERVER['remote-addr'] = '8.8.4.4';
TestRouter::router()->reset();
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/fail', 'DummyController@method1');
});
TestRouter::debug('/fail');
}
public function testIpSuccess() {
global $_SERVER;
// Test ip that is not blocked
$_SERVER['remote-addr'] = '6.6.6.6';
TestRouter::router()->reset();
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/success', 'DummyController@method1');
});
TestRouter::debug('/success');
// Test ip in whitelist
$_SERVER['remote-addr'] = '8.8.2.2';
TestRouter::router()->reset();
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/success', 'DummyController@method1');
});
TestRouter::debug('/success');
$this->assertTrue(true);
}
}
@@ -1,26 +0,0 @@
<?php
class CustomClassLoader implements \Pecee\SimpleRouter\ClassLoader\IClassLoader
{
public function loadClass(string $class)
{
return new DummyController();
}
/**
* Called when loading class method
* @param object $class
* @param string $method
* @param array $parameters
* @return object
*/
public function loadClassMethod($class, string $method, array $parameters)
{
return call_user_func_array([$class, $method], [true]);
}
public function loadClosure(callable $closure, array $parameters)
{
return call_user_func_array($closure, [true]);
}
}
@@ -1,18 +0,0 @@
<?php
class DummyCsrfVerifier extends \Pecee\Http\Middleware\BaseCsrfVerifier {
protected $except = [
'/exclude-page',
'/exclude-all/*',
];
protected $include = [
'/exclude-all/include-page',
];
public function testSkip(\Pecee\Http\Request $request) {
return $this->skip($request);
}
}
@@ -1,46 +0,0 @@
<?php
class DummyController
{
public function index()
{
}
public function method1()
{
}
public function method2()
{
}
public function method3()
{
return 'method3';
}
public function param($params = null)
{
echo join(', ', func_get_args());
}
public function getTest()
{
echo 'getTest';
}
public function postTest()
{
echo 'postTest';
}
public function putTest()
{
echo 'putTest';
}
}
@@ -1,13 +0,0 @@
<?php
require_once 'Exception/MiddlewareLoadedException.php';
use Pecee\Http\Request;
class DummyMiddleware implements \Pecee\Http\Middleware\IMiddleware
{
public function handle(Request $request) : void
{
throw new MiddlewareLoadedException('Middleware loaded!');
}
}
@@ -1,5 +0,0 @@
<?php
class ExceptionHandlerException extends \Exception
{
}
@@ -1,4 +0,0 @@
<?php
class MiddlewareLoadedException extends \Exception
{
}
@@ -1,18 +0,0 @@
<?php
class ResponseException extends \Exception
{
protected $response;
public function __construct($response)
{
$this->response = $response;
parent::__construct('', 0);
}
public function getResponse()
{
return $this->response;
}
}
@@ -1,10 +0,0 @@
<?php
class ExceptionHandler implements \Pecee\SimpleRouter\Handlers\IExceptionHandler
{
public function handleError(\Pecee\Http\Request $request, \Exception $error) : void
{
echo $error->getMessage();
}
}
@@ -1,13 +0,0 @@
<?php
class ExceptionHandlerFirst implements \Pecee\SimpleRouter\Handlers\IExceptionHandler
{
public function handleError(\Pecee\Http\Request $request, \Exception $error) : void
{
global $stack;
$stack[] = static::class;
$request->setUrl(new \Pecee\Http\Url('/'));
}
}
@@ -1,13 +0,0 @@
<?php
class ExceptionHandlerSecond implements \Pecee\SimpleRouter\Handlers\IExceptionHandler
{
public function handleError(\Pecee\Http\Request $request, \Exception $error) : void
{
global $stack;
$stack[] = static::class;
$request->setUrl(new \Pecee\Http\Url('/'));
}
}
@@ -1,13 +0,0 @@
<?php
class ExceptionHandlerThird implements \Pecee\SimpleRouter\Handlers\IExceptionHandler
{
public function handleError(\Pecee\Http\Request $request, \Exception $error) : void
{
global $stack;
$stack[] = static::class;
throw new ResponseException('ExceptionHandler loaded');
}
}
@@ -1,26 +0,0 @@
<?php
class FindUrlBootManager implements \Pecee\SimpleRouter\IRouterBootManager
{
protected $result;
public function __construct(&$result)
{
$this->result = &$result;
}
/**
* Called when router loads it's routes
*
* @param \Pecee\SimpleRouter\Router $router
* @param \Pecee\Http\Request $request
*/
public function boot(\Pecee\SimpleRouter\Router $router, \Pecee\Http\Request $request): void
{
$contact = $router->findRoute('contact');
if($contact !== null) {
$this->result = true;
}
}
}
@@ -1,30 +0,0 @@
<?php
class TestBootManager implements \Pecee\SimpleRouter\IRouterBootManager
{
protected $rewrite;
public function __construct(array $rewrite)
{
$this->rewrite = $rewrite;
}
/**
* Called when router loads it's routes
*
* @param \Pecee\SimpleRouter\Router $router
* @param \Pecee\Http\Request $request
*/
public function boot(\Pecee\SimpleRouter\Router $router, \Pecee\Http\Request $request): void
{
foreach ($this->rewrite as $url => $rewrite) {
// If the current url matches the rewrite url, we use our custom route
if ($request->getUrl()->contains($url) === true) {
$request->setRewriteUrl($rewrite);
}
}
}
}
@@ -1,14 +0,0 @@
<?php
class IpRestrictMiddleware extends \Pecee\Http\Middleware\IpRestrictAccess {
protected $ipBlacklist = [
'5.5.5.5',
'8.8.*',
];
protected $ipWhitelist = [
'8.8.2.2',
];
}

Some files were not shown because too many files have changed in this diff Show More