Compare commits

...

35 Commits

Author SHA1 Message Date
Simon Sessingø 810b80487d Merge pull request #97 from skipperbent/development
- Added custom ExceptionHandler example to documentation.
2016-04-21 08:30:34 +02:00
Simon Sessingø 1420203149 - Added custom ExceptionHandler example to documentation.
- Fixed reference to request() helper in Input class.
- Changed RouterBase handleException method to support 404-exceptions.
2016-04-21 07:16:29 +02:00
Simon Sessingø 18a9df56ca Merge pull request #96 from skipperbent/development
Development
2016-04-20 08:10:18 +02:00
Simon Sessingø f7af53a9af - Optimized Input class to ensure that InputFile items are always returned as object as they contain no value. 2016-04-19 14:48:26 +02:00
Simon Sessingø 6b8351f1b8 - Input class no longer tries to search for parameter in FILE or POST if it's not a postback. 2016-04-19 02:08:14 +02:00
Simon Sessingø 6e14ded03f Merge pull request #95 from skipperbent/development
Development
2016-04-16 23:23:56 +02:00
Simon Sessingø eb3ddf2bf7 [OPTIMISATION] Optimized get method in Input class to trim value and return default value if empty. 2016-04-16 21:34:22 +02:00
Simon Sessingø 2afe784f47 [BUGFIX] Fixed middlewaresToLoad logic used before any routes loaded in RouterBase. 2016-04-16 13:08:32 +02:00
Simon Sessingø 899081f8d8 Merge pull request #94 from skipperbent/development
Update README.md
2016-04-16 00:01:22 +02:00
Simon Sessingø 94e98ad5f0 Update README.md 2016-04-16 00:01:12 +02:00
Simon Sessingø e7b9206bc9 Merge pull request #93 from skipperbent/development
Development
2016-04-15 23:21:00 +02:00
Simon Sessingø 8f24256434 Merge pull request #92 from skipperbent/feature-input
Removed old input method from Request class.
2016-04-15 23:20:50 +02:00
Simon Sessingø ec355c90b5 Removed old input method from Request class. 2016-04-15 23:20:16 +02:00
Simon Sessingø cd6e800984 Merge pull request #91 from skipperbent/development
Development
2016-04-15 23:14:55 +02:00
Simon Sessingø a8633b5c51 Merge pull request #90 from skipperbent/feature-input
Feature input
2016-04-15 23:12:40 +02:00
Simon Sessingø 7fdeef74d6 InputFile now inherits from InputItem 2016-04-15 23:10:49 +02:00
Simon Sessingø 17adfb8aa4 [FEATURE] Added Input classes from pecee-framework.
- Updated documentation to reflect new changes.
2016-04-15 23:07:51 +02:00
Simon Sessingø 7a429cec1d [BUGFIX] Fixed get-parameters as array (param[id]=value) causing array-to-string notice. 2016-04-14 23:52:08 +02:00
Simon Sessingø 3da7c4b446 - Fixed getIp in HttpRequest class not picking up local-ip.
- Made setdefaultNamespace method chainable.
- Simplified SimpleRouter class.
2016-04-11 22:37:15 +02:00
Simon Sessingø 11bd5a7d11 Merge pull request #89 from skipperbent/development
[BUGFIX] Fixed notice
2016-04-09 15:39:12 +02:00
Simon Sessingø f3ac9dc47c [BUGFIX] Fixed notice 2016-04-09 15:38:45 +02:00
Simon Sessingø be32796b01 Merge pull request #88 from skipperbent/development
[TASK] Moved group-middleware rendering to routeRequest to ensure all…
2016-04-09 15:32:45 +02:00
Simon Sessingø 22563671c5 [TASK] Moved group-middleware rendering to routeRequest to ensure all route-urls has been initialised.
- Optimisations.
2016-04-09 15:26:18 +02:00
Simon Sessingø 8b3d71a328 Merge pull request #87 from skipperbent/development
[BUGFIX] Fixed only match group route if prefix is set
2016-04-09 10:02:22 +02:00
Simon Sessingø 491e920cfc [BUGFIX] Fixed only match group route if prefix is set 2016-04-09 10:01:02 +02:00
Simon Sessingø 1fae638aaf Merge pull request #86 from skipperbent/development
Development
2016-04-09 09:50:56 +02:00
Simon Sessingø 6cef099110 [TASK] Updated documentation with list of sites that uses project in production 2016-04-09 09:49:59 +02:00
Simon Sessingø 5f95290e4b [OPTIMISATION] Optimised exception-message thrown when not using IExceptionHandler interface. 2016-04-09 09:45:39 +02:00
Simon Sessingø 7234415e24 [TASK] Made abstract ExceptionHandler class an interface.
- Updated documentation to reflect new changes.
2016-04-09 09:42:57 +02:00
Simon Sessingø 257875c6f9 [TASK] Load group middleware if prefix matches
- Middlewares from groups are now loaded if prefix matches.
- Optimisations.
2016-04-09 09:25:41 +02:00
Simon Sessingø 9b743e6e57 [FEATURE] Added requirements (issue: #80) 2016-04-09 08:05:18 +02:00
Simon Sessingø 457dbc5710 [BUGFIX] Fixed issue #83
- Added more tests.
2016-04-09 07:51:03 +02:00
Simon Sessingø fc4fd0edf1 [FEATURE] Added ExceptionHandling functionality + tests.
- Somehow the ExceptionHandling functionality never really get implemented and/or got lost in translation, it's back whup!
- Added some basic tests for Middleware + routing.
2016-04-09 07:05:49 +02:00
Simon Sessingø 2f2c3ca3ca [OPTIMISATIONS] Optimised code and removed unused references 2016-04-09 05:58:18 +02:00
Simon Sessingø b34738a51a [OPTIMISATION] Optimised CSRF-token management. 2016-04-09 05:51:27 +02:00
24 changed files with 840 additions and 126 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
.idea
composer.lock
composer.lock
vendor/
+121 -21
View File
@@ -5,9 +5,13 @@ Simple, fast and yet powerful PHP router that is easy to get integrated and in a
Add the latest version of Simple PHP Router running this command.
```
composer require pecee/framework
composer require pecee/simple-router
```
## Requirements
- PHP 5.4 or greater
## Notes
The goal of this project is to create a router that is 100% compatible with the Laravel documentation, but as simple as possible and as easy to integrate and change as possible.
@@ -26,6 +30,7 @@ The goal of this project is to create a router that is 100% compatible with the
- Optional parameters
- Sub-domain routing
- Custom boot managers to redirect urls to other routes
- Input manager; to manage `GET`, `POST` params.
## Initialising the router
@@ -34,7 +39,7 @@ In your ```index.php``` require your ```routes.php``` and call the ```routeReque
This is an example of a basic ```index.php``` file:
```php
use \Pecee\SimpleRouter;
use \Pecee\SimpleRouter\SimpleRouter;
require_once 'routes.php'; // change this to whatever makes sense in your project
@@ -42,7 +47,7 @@ require_once 'routes.php'; // change this to whatever makes sense in your projec
$defaultControllerNamespace = 'MyWebsite\\Controller';
// Do the routing
SimpleRouter::init($defaultControllerNamespace);
SimpleRouter::start($defaultControllerNamespace);
```
## Adding routes
@@ -51,6 +56,9 @@ This router is heavily inspired by the Laravel 5.* router, so anything you find
### Basic example
- ExceptionsHandlers must implement the `IExceptionHandler` interface.
- Middlewares must implement the `IMiddleware` interface.
```php
use Pecee\SimpleRouter\SimpleRouter;
@@ -62,6 +70,9 @@ use Pecee\SimpleRouter\SimpleRouter;
* the request, for instance if a user is not authenticated.
*/
// Add CSRF support (if needed)
SimpleRouter::csrfVerifier(new \Pecee\Http\Middleware\BaseCsrfVerifier());
SimpleRouter::group(['prefix' => 'v1', 'middleware' => '\MyWebsite\Middleware\SomeMiddlewareClass'], function() {
SimpleRouter::group(['prefix' => '/services', 'exceptionHandler' => '\MyProject\Handler\CustomExceptionHandler'], function() {
@@ -92,6 +103,37 @@ SimpleRouter::group(['prefix' => 'v1', 'middleware' => '\MyWebsite\Middleware\So
});
```
#### ExceptionHandler example
This is a basic example of an ExceptionHandler implementation:
```php
namespace BB\Handlers;
use Pecee\Http\Request;
use Pecee\SimpleRouter\RouterEntry;
class CustomExceptionHandler implements IExceptionHandler {
public function handleError( Request $request, RouterEntry $router = null, \Exception $error) {
// If the error-code is 404; show another route which contains the page-not-found
if($error->getCode() === 404) {
// Load your custom 404-page view
}
// Output error as json if on api path.
if(stripos($request->getUri(), '/api') !== false) {
response()->json(['error' => $error->getMessage()]);
}
// Otherwise default exception will be thrown by the router.
}
}
``
### Sub-domain routing
Route groups may also be used to route wildcard sub-domains. Sub-domains may be assigned route parameters just like route URIs, allowing you to capture a portion of the sub-domain for usage in your route or controller. The sub-domain may be specified using the ```domain``` key on the group attribute array:
@@ -160,23 +202,6 @@ class Router extends SimpleRouter {
parent::start($defaultNamespace);
} catch(\Exception $e) {
$route = RouterBase::getInstance()->getLoadedRoute();
$exceptionHandler = null;
// Load and use exception-handler defined on group
if($route && $route->getGroup()) {
$exceptionHandler = $route->getGroup()->getExceptionHandler();
if($exceptionHandler !== null) {
$class = new $exceptionHandler();
$class->handleError(RouterBase::getInstance()->getRequest(), $route, $e);
}
}
// Otherwise use the fallback default exceptions handler
if(self::$defaultExceptionHandler !== null) {
$class = new self::$defaultExceptionHandler();
$class->handleError(RouterBase::getInstance()->getRequest(), $route, $e);
@@ -264,7 +289,7 @@ SimpleRouter::csrfVerifier(new \Demo\Middleware\CsrfVerifier());
Sometimes it can be necessary to keep urls stored in the database, file or similar. In this example, we want the url ```/my-cat-is-beatiful``` to load the route ```/article/view/1``` which the router knows, because it's defined in the ```routes.php``` file.
To interfere with the router, we create a class that inherits from ```RouterBootManager```. This class will be loaded before any other rules in ```routes.php``` and allow us to "change" the current route, if any of our criteria are fulfilled (like comming from the url ```/my-cat-is-beatiful```).
To interfere with the router, we create a class that inherits from ```RouterBootManager```. This class will be loaded before any other rules in ```routes.php``` and allow us to "change" the current route, if any of our criteria are fulfilled (like coming from the url ```/my-cat-is-beatiful```).
```php
@@ -332,6 +357,81 @@ $route->setClass('Example\MyCustomClass');
$route->setMethod('hello');
```
## Using the Input class to manage parameters
We've added the `Input` class to easy access parameters from your Controller-classes.
**Return single parameter value (matches both GET, POST, FILE):**
```php
$value = Request::getInstance()->getInput()->get('name');
```
**Return parameter object (matches both GET, POST, FILE):**
```php
$object = Request::getInstance()->getInput()->getObject('name');
```
**Return specific GET parameter (where name is the name of your parameter):**
```php
$object = Request::getInstance()->getInput()->get->name;
$object = Request::getInstance()->getInput()->post->name;
$object = Request::getInstance()->getInput()->file->name;
```
**Return all parameters:**
```php
// Get all
$objects = Request::getInstance()->getInput()->all();
// Only match certain keys
$objects = Request::getInstance()->getInput()->all([
'company_name',
'user_id'
]);
```
All object inherits from `InputItem` class and will always contain these methods:
- `getValue()` - returns the value of the input.
- `getIndex()` - returns the index/key of the input.
- `getName()` - returns a human friendly name for the input (company_name will be Company Name etc).
`InputFile` has the same methods as above along with some other file-specific methods like:
- `getTmpName()` - get file temporary name.
- `getSize()` - get file size.
- `move($destination)` - move file to destination.
- `getContents()` - get file content.
- `getType()` - get mime-type for file.
- `getError()` - get file upload error.
### Easy access your input
Create a helper function to easily get access to the input elements.
Example:
```php
/**
* Get input class
* @return \Pecee\Http\Input\Input
*/
function input() {
return \Pecee\Http\Request::getInstance()->getInput();
}
```
Then you can easily do something like this in your controller:
```php
// Get parameter site_id or default-value 2
$value = input()->get('site_id', '2');
```
## Sites
This is some sites that uses the simple-router project in production.
- [holla.dk](http://www.holla.dk)
- [ninjaimg.com](http://ninjaimg.com)
## Documentation
While I work on a better documentation, please refer to the Laravel 5 routing documentation here:
+1 -1
View File
@@ -13,7 +13,7 @@
}
],
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "4.7.7"
+3 -3
View File
@@ -37,7 +37,7 @@ class CsrfToken {
* @param $token
*/
public function setToken($token) {
setcookie(self::CSRF_KEY, $token, time() + 60 * 120, '/');
setcookie(static::CSRF_KEY, $token, time() + 60 * 120, '/');
}
/**
@@ -46,7 +46,7 @@ class CsrfToken {
*/
public function getToken(){
if($this->hasToken()) {
return $_COOKIE[self::CSRF_KEY];
return $_COOKIE[static::CSRF_KEY];
}
return null;
}
@@ -56,7 +56,7 @@ class CsrfToken {
* @return bool
*/
public function hasToken() {
return isset($_COOKIE[self::CSRF_KEY]);
return isset($_COOKIE[static::CSRF_KEY]);
}
}
+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);
}
+197
View File
@@ -0,0 +1,197 @@
<?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;
public function __construct() {
$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 = $this->get->getData();
$output = array_merge($output, $this->post->getData());
if($filter !== null) {
$tmp = array();
foreach($output as $key => $val) {
if(in_array($key, $filter)) {
$tmp[$key] = $val;
}
}
return $tmp;
}
return $output;
}
public function getObject($index, $default = null) {
$index = (strpos($index, '[') > -1) ? substr($index, 0, strpos($index, '[')) : $index;
$element = $this->get->findFirst($index);
if($element !== null) {
return $element;
}
if(Request::getInstance()->getMethod() !== 'get') {
$element = $this->post->findFirst($index);
if ($element !== null) {
return $element;
}
$element = $this->file->findFirst($index);
if ($element !== null) {
return $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) {
$key = (strpos($index, '[') > -1) ? substr($index, strpos($index, '[')+1, strpos($index, ']') - strlen($index)) : null;
$index = (strpos($index, '[') > -1) ? substr($index, 0, strpos($index, '[')) : $index;
$item = $this->getObject($index);
if($item !== null) {
if($item instanceof InputFile) {
return $item;
}
if (is_array($item->getValue())) {
return ($key !== null && isset($item->getValue()[$key])) ? $item->getValue()[$key] : $item->getValue();
}
return (trim($item->getValue()) === '') ? $default : $item->getValue();
}
return $default;
}
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 = array();
foreach($get as $k => $g) {
$output[$k] = new InputItem($k, $g);
}
$this->get->{$key} = new InputItem($key, $output);
}
}
}
public function setPost() {
$this->post = new InputCollection();
$postVars = array();
if(in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'PATCH', 'DELETE'])) {
parse_str(file_get_contents('php://input'), $postVars);
} else {
$postVars = $_POST;
}
if(count($postVars)) {
foreach($postVars as $key => $post) {
if(!is_array($post)) {
$this->post->{strtolower($key)} = new InputItem($key, $post);
continue;
}
$output = array();
foreach($post as $k=>$p) {
$output[$k] = new InputItem($k, $p);
}
$this->post->{strtolower($key)} = new InputItem($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 = array();
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)} = new InputItem($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);
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace Pecee\Http\Input;
class InputFile extends InputItem {
protected $name;
protected $size;
protected $type;
protected $error;
protected $tmpName;
/**
* @return string
*/
public function getSize() {
return $this->size;
}
/**
* @return string
*/
public function getType() {
return $this->type;
}
/**
* @return string
*/
public function getError() {
return $this->error;
}
/**
* @return string
*/
public function getTmpName() {
return $this->tmpName;
}
public function getExtension() {
return pathinfo($this->getName(), PATHINFO_EXTENSION);
}
public function move($destination) {
return move_uploaded_file($this->tmpName, $destination);
}
public function getContents() {
return file_get_contents($this->tmpName);
}
public function setTmpName($name) {
$this->tmpName = $name;
}
public function setSize($size) {
$this->size = $size;
}
public function setType($type) {
$this->type = $type;
}
public function setError($error) {
$this->error = $error;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace Pecee\Http\Input;
class InputItem {
protected $index;
protected $name;
protected $value;
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('_', ' ', $this->index));
}
/**
* @return array
*/
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 $this
*/
public function setName($name) {
$this->name = $name;
return $this;
}
public function __toString() {
return (string)$this->getValue();
}
}
+25 -4
View File
@@ -12,10 +12,13 @@ class BaseCsrfVerifier implements IMiddleware {
protected $except;
protected $csrfToken;
protected $token;
public function __construct() {
$this->csrfToken = new CsrfToken();
// Generate or get the CSRF-Token from Cookie.
$this->token = (!$this->hasToken()) ? $this->generateToken() : $this->csrfToken->getToken();
}
/**
@@ -50,14 +53,14 @@ class BaseCsrfVerifier implements IMiddleware {
if($request->getMethod() != 'get' && !$this->skip($request)) {
$token = (isset($_POST[self::POST_KEY])) ? $_POST[self::POST_KEY] : null;
$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(self::HEADER_KEY);
$token = $request->getHeader(static::HEADER_KEY);
}
if( !$this->csrfToken->validate( $token ) ) {
if( !$this->csrfToken->validate($token) ) {
throw new TokenMismatchException('Invalid csrf-token.');
}
@@ -65,4 +68,22 @@ class BaseCsrfVerifier implements IMiddleware {
}
public function generateToken() {
$token = $this->csrfToken->generateToken();
$this->csrfToken->setToken($token);
return $token;
}
public function hasToken() {
if($this->token != null) {
return true;
}
return $this->csrfToken->hasToken();
}
public function getToken() {
return $this->token;
}
}
+8 -7
View File
@@ -1,6 +1,8 @@
<?php
namespace Pecee\Http;
use Pecee\Http\Input\Input;
class Request {
protected static $instance;
@@ -24,6 +26,7 @@ class Request {
$this->uri = $_SERVER['REQUEST_URI'];
$this->method = (isset($_POST['_method'])) ? strtolower($_POST['_method']) : strtolower($_SERVER['REQUEST_METHOD']);
$this->headers = $this->getAllHeaders();
$this->input = new Input();
}
protected function getAllHeaders() {
@@ -93,7 +96,7 @@ class Request {
* @return string
*/
public function getIp() {
return isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
return ((isset($_SERVER['HTTP_X_FORWARDED_FOR']) && strlen($_SERVER['HTTP_X_FORWARDED_FOR'])) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR']);
}
/**
@@ -122,13 +125,11 @@ class Request {
}
/**
* Get request input or default value
* @param string $name
* @param string $defaultValue
* @return mixed
* Get input class
* @return Input
*/
public function getInput($name, $defaultValue) {
return (isset($_REQUEST[$name]) ? $_REQUEST[$name] : $defaultValue);
public function getInput() {
return $this->input;
}
public function isFormatAccepted($format) {
+58 -25
View File
@@ -1,8 +1,8 @@
<?php
namespace Pecee\SimpleRouter;
use Pecee\CsrfToken;
use Pecee\Exception\RouterException;
use Pecee\Handler\IExceptionHandler;
use Pecee\Http\Middleware\BaseCsrfVerifier;
use Pecee\Http\Request;
@@ -19,20 +19,19 @@ class RouterBase {
protected $defaultNamespace;
protected $bootManagers;
protected $baseCsrfVerifier;
protected $middlewaresToLoad;
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->baseCsrfVerifier = new BaseCsrfVerifier();
$this->request = Request::getInstance();
$this->bootManagers = array();
$csrf = new CsrfToken();
$token = ($csrf->hasToken()) ? $csrf->getToken() : $csrf->generateToken();
$csrf->setToken($token);
$this->middlewaresToLoad = array();
$this->exceptionHandlers = array();
}
public function addRoute(RouterEntry $route) {
@@ -61,11 +60,9 @@ class RouterBase {
}
if($this->defaultNamespace && !$route->getNamespace()) {
$namespace = null;
$namespace = $this->defaultNamespace;
if ($route->getNamespace()) {
$namespace = $this->defaultNamespace . '\\' . $route->getNamespace();
} else {
$namespace = $this->defaultNamespace;
$namespace .= '\\' . $route->getNamespace();
}
$route->setNamespace($namespace);
@@ -73,13 +70,13 @@ class RouterBase {
$newPrefixes = $prefixes;
if($route->getPrefix()) {
array_push($newPrefixes, rtrim($route->getPrefix(), '/'));
if($route->getPrefix() && trim($route->getPrefix(), '/') !== '') {
array_push($newPrefixes, trim($route->getPrefix(), '/'));
}
if(!($route instanceof RouterGroup)) {
if(is_array($newPrefixes) && count($newPrefixes) && $backStack) {
$route->setUrl( join('/', $newPrefixes) . $route->getUrl() );
$route->setUrl( '/' . join('/', $newPrefixes) . $route->getUrl() );
}
$group = null;
@@ -90,8 +87,17 @@ class RouterBase {
if($route instanceof RouterGroup && is_callable($route->getCallback())) {
$group = $route;
$route->renderRoute($this->request);
$mergedSettings = array_merge($settings, $route->getMergeableSettings());
// Load middleware on group if route matches
if($route->getPrefix() !== null && $route->matchRoute($this->request)) {
if($route->getExceptionHandler() !== null) {
$this->exceptionHandlers[] = $route->getExceptionHandler();
}
$this->middlewaresToLoad[] = $route;
}
}
$this->currentRoute = null;
@@ -124,15 +130,18 @@ class RouterBase {
// Verify csrf token for request
if($this->baseCsrfVerifier !== null) {
/* @var $csrfVerifier BaseCsrfVerifier */
$csrfVerifier = $this->baseCsrfVerifier;
$csrfVerifier = new $csrfVerifier();
$csrfVerifier->handle($this->request);
$this->baseCsrfVerifier->handle($this->request);
}
// Loop through each route-request
$this->processRoutes($this->routes);
// Load group middlewares
/* @var $route RouterEntry */
foreach($this->middlewaresToLoad as $route) {
$route->loadMiddleware($this->request);
}
$routeNotAllowed = false;
// Make sure routes with longer urls are rendered first
@@ -167,20 +176,39 @@ class RouterBase {
$this->request->loadedRoute = $route;
$route->loadMiddleware($this->request);
$this->request->loadedRoute->renderRoute($this->request);
try {
$this->request->loadedRoute->renderRoute($this->request);
} catch(\Exception $e) {
$this->handleException($e);
}
break;
}
}
if($routeNotAllowed) {
throw new RouterException('Route or method not allowed', 403);
$this->handleException(new RouterException('Route or method not allowed', 403));
}
if(!$this->request->loadedRoute) {
throw new RouterException(sprintf('Route not found: %s', $this->request->getUri()), 404);
$this->handleException(new RouterException(sprintf('Route not found: %s', $this->request->getUri()), 404));
}
}
protected function handleException(\Exception $e) {
foreach ($this->exceptionHandlers as $handler) {
$handler = new $handler($this->request);
if (!($handler instanceof IExceptionHandler)) {
throw new RouterException('Exception handler must implement the IExceptionHandler interface.');
}
$handler->handleError($this->request, $this->request->loadedRoute, $e);
}
throw $e;
}
/**
* @return string
*/
@@ -190,9 +218,11 @@ class RouterBase {
/**
* @param string $defaultNamespace
* @return static
*/
public function setDefaultNamespace($defaultNamespace) {
$this->defaultNamespace = $defaultNamespace;
return $this;
}
/**
@@ -275,8 +305,8 @@ class RouterBase {
public function arrayToParams(array $getParams = null, $includeEmpty = true) {
if (is_array($getParams) && count($getParams) > 0) {
foreach ($getParams as $key => $val) {
if (!empty($val) || empty($val) && $includeEmpty) {
$getParams[$key] = $key . '=' . $val;
if (!empty($val) || $includeEmpty) {
$getParams[$key] = (is_array($val) ? $this->arrayToParams($val, $includeEmpty) : $key . '=' . $val);
}
}
return join('&', $getParams);
@@ -284,7 +314,7 @@ class RouterBase {
return '';
}
protected function processUrl($route, $method = null, $parameters = null, $getParams = null) {
protected function processUrl(RouterRoute $route, $method = null, $parameters = null, $getParams = null) {
$domain = '';
@@ -431,7 +461,6 @@ class RouterBase {
$url = '/' . trim(join('/', $url), '/') . '/';
if($getParams !== null && count($getParams)) {
$url .= '?' . $this->arrayToParams($getParams);
}
@@ -446,4 +475,8 @@ class RouterBase {
return self::$instance;
}
public static function reset() {
self::$instance = null;
}
}
@@ -79,10 +79,12 @@ class RouterController extends RouterEntry {
/**
* @param string $url
* @return static
*/
public function setUrl($url) {
$url = rtrim($url, '/') . '/';
$this->url = $url;
return $this;
}
/**
@@ -94,9 +96,11 @@ class RouterController extends RouterEntry {
/**
* @param string $controller
* @return static
*/
public function setController($controller) {
$this->controller = $controller;
return $this;
}
/**
@@ -108,9 +112,11 @@ class RouterController extends RouterEntry {
/**
* @param string $method
* @return static
*/
public function setMethod($method) {
$this->method = $method;
return $this;
}
}
+2 -4
View File
@@ -93,7 +93,7 @@ abstract class RouterEntry {
* @return self
*/
public function setPrefix($prefix) {
$this->prefix = '/' . trim($prefix, '/') . '/';
$this->prefix = '/' . ltrim($prefix, '/');
return $this;
}
@@ -123,7 +123,7 @@ abstract class RouterEntry {
}
/**
* @return string
* @return string|array
*/
public function getMiddleware() {
return $this->middleware;
@@ -360,9 +360,7 @@ abstract class RouterEntry {
}
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 {
-4
View File
@@ -7,10 +7,6 @@ use Pecee\Http\Request;
class RouterGroup extends RouterEntry {
public function __construct() {
parent::__construct();
}
public function matchDomain(Request $request) {
if($this->domain !== null) {
@@ -109,10 +109,12 @@ class RouterResource extends RouterEntry {
/**
* @param string $url
* @return static
*/
public function setUrl($url) {
$url = rtrim($url, '/') . '/';
$this->url = $url;
return $this;
}
/**
@@ -124,9 +126,11 @@ class RouterResource extends RouterEntry {
/**
* @param string $controller
* @return static
*/
public function setController($controller) {
$this->controller = $controller;
return $this;
}
}
+2 -5
View File
@@ -2,7 +2,6 @@
namespace Pecee\SimpleRouter;
use Pecee\ArrayUtil;
use Pecee\Http\Request;
class RouterRoute extends RouterEntry {
@@ -104,11 +103,9 @@ class RouterRoute extends RouterEntry {
}
}
} else {
if(strtolower($this->getAlias()) === strtolower($name)) {
return true;
}
return strtolower($this->getAlias()) === strtolower($name);
}
return false;
}
+31 -51
View File
@@ -16,12 +16,10 @@ class SimpleRouter {
/**
* Start/route request
* @param null $defaultNamespace
* @throws RouterException
* @throws \Pecee\Exception\RouterException
*/
public static function start($defaultNamespace = null) {
$router = RouterBase::getInstance();
$router->setDefaultNamespace($defaultNamespace);
$router->routeRequest();
RouterBase::getInstance()->setDefaultNamespace($defaultNamespace)->routeRequest();
}
/**
@@ -37,47 +35,19 @@ class SimpleRouter {
}
public static function get($url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
$route->addSettings($settings);
$route->setRequestMethods(array(RouterRoute::REQUEST_TYPE_GET));
$router = RouterBase::getInstance();
$router->addRoute($route);
return $route;
return self::match(['get'], $url, $callback, $settings);
}
public static function post($url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
$route->addSettings($settings);
$route->setRequestMethods(array(RouterRoute::REQUEST_TYPE_POST));
$router = RouterBase::getInstance();
$router->addRoute($route);
return $route;
return self::match(['post'], $url, $callback, $settings);
}
public static function put($url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
$route->addSettings($settings);
$route->setRequestMethods(array(RouterRoute::REQUEST_TYPE_PUT, RouterRoute::REQUEST_TYPE_PATCH));
$router = RouterBase::getInstance();
$router->addRoute($route);
return $route;
return self::match(['put'], $url, $callback, $settings);
}
public static function delete($url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
$route->addSettings($settings);
$route->setRequestMethods(array(RouterRoute::REQUEST_TYPE_DELETE));
$router = RouterBase::getInstance();
$router->addRoute($route);
return $route;
return self::match(['delete'], $url, $callback, $settings);
}
public static function group($settings = array(), $callback) {
@@ -88,8 +58,7 @@ class SimpleRouter {
$group->setSettings($settings);
}
$router = RouterBase::getInstance();
$router->addRoute($group);
RouterBase::getInstance()->addRoute($group);
return $group;
}
@@ -98,7 +67,7 @@ class SimpleRouter {
* Adds get + post route
*
* @param string $url
* @param function $callback
* @param callable $callback
* @param array|null $settings
* @return RouterRoute
*/
@@ -109,37 +78,48 @@ class SimpleRouter {
public static function match(array $requestMethods, $url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
$route->setRequestMethods($requestMethods);
$route->addSettings($settings);
$router = RouterBase::getInstance();
$router->addRoute($route);
if($settings !== null) {
$route->addSettings($settings);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
public static function all($url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback);
$route->addSettings($settings);
$router = RouterBase::getInstance();
$router->addRoute($route);
if($settings !== null) {
$route->addSettings($settings);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
public static function controller($url, $controller, array $settings = null) {
$route = new RouterController($url, $controller);
$route->addSettings($settings);
$router = RouterBase::getInstance();
$router->addRoute($route);
if($settings !== null) {
$route->addSettings($settings);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
public static function resource($url, $controller, array $settings = null) {
$route = new RouterResource($url, $controller);
$route->addSettings($settings);
$router = RouterBase::getInstance();
$router->addRoute($route);
if($settings !== null) {
$route->addSettings($settings);
}
RouterBase::getInstance()->addRoute($route);
return $route;
}
+9
View File
@@ -0,0 +1,9 @@
<?php
class DummyController {
public function start() {
echo static::class . '@' .'start() OK';
}
}
+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 {}
+51
View File
@@ -0,0 +1,51 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
class GroupTest extends PHPUnit_Framework_TestCase {
protected $result;
public function __construct() {
// Initial setup
$_SERVER['HTTP_HOST'] = 'example.com';
$_SERVER['REQUEST_URI'] = '/api/v1/test';
$_SERVER['REQUEST_METHOD'] = 'get';
}
protected function group() {
$this->result = true;
}
public function testGroup() {
\Pecee\SimpleRouter\RouterBase::reset();
$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::reset();
\Pecee\Http\Request::getInstance()->setUri('/api/v1/test');
\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();
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
class MiddlewareTest extends PHPUnit_Framework_TestCase {
public function __construct() {
// Initial setup
$_SERVER['HTTP_HOST'] = 'example.com';
$_SERVER['REQUEST_URI'] = '/my/test/url';
$_SERVER['REQUEST_METHOD'] = 'get';
}
public function testMiddlewareFound() {
\Pecee\Http\Request::getInstance()->setMethod('get');
\Pecee\SimpleRouter\RouterBase::reset();
\Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start', ['middleware' => 'DummyMiddleware']);
try {
\Pecee\SimpleRouter\SimpleRouter::start();
}catch(Exception $e) {
$this->assertTrue(($e instanceof MiddlewareLoadedException));
return;
}
throw new Exception('Middleware not loaded');
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
require_once 'Dummy/DummyController.php';
class RouterRouteTest extends PHPUnit_Framework_TestCase {
public function __construct() {
// Initial setup
$_SERVER['HTTP_HOST'] = 'example.com';
$_SERVER['REQUEST_URI'] = '/my/test/url';
$_SERVER['REQUEST_METHOD'] = 'get';
}
public function testGet() {
\Pecee\SimpleRouter\RouterBase::reset();
\Pecee\Http\Request::getInstance()->setMethod('get');
\Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testPost() {
\Pecee\Http\Request::getInstance()->setMethod('post');
\Pecee\SimpleRouter\RouterBase::reset();
\Pecee\SimpleRouter\SimpleRouter::post('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testPut() {
\Pecee\Http\Request::getInstance()->setMethod('put');
\Pecee\SimpleRouter\RouterBase::reset();
\Pecee\SimpleRouter\SimpleRouter::put('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testDelete() {
\Pecee\Http\Request::getInstance()->setMethod('delete');
\Pecee\SimpleRouter\RouterBase::reset();
\Pecee\SimpleRouter\SimpleRouter::delete('/my/test/url', 'DummyController@start');
\Pecee\SimpleRouter\SimpleRouter::start();
}
public function testMethodNotAllowed() {
\Pecee\SimpleRouter\RouterBase::reset();
\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());
}
}
}