Development

- Added dependency injection support.
- Added php-di composer dependency.
- Added `ClassLoader` class.
- Added `IClassLoader` interface.
- Added unit-tests for dependency injection.
- Updated documentation to reflect new features.
This commit is contained in:
Simon Sessingø
2018-03-29 21:16:02 +02:00
parent cca2f5cb88
commit af2ac6031d
17 changed files with 382 additions and 68 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
.idea
composer.lock
vendor/
vendor/
tests/tmp/*
+87 -2
View File
@@ -1,7 +1,7 @@
# simple-router
Simple, fast and yet powerful PHP router that is easy to get integrated and in any project.
Heavily inspired by the way Laravel handles routing, with both simplicity and expandability in mind.
Heavily inspired by the way Laravel handles routing, with both simplicity and expand-ability in mind.
### Support the project
@@ -43,6 +43,9 @@ You can donate any amount of your choice by [clicking here](https://www.paypal.c
- [Partial groups](#partial-groups)
- [Form Method Spoofing](#form-method-spoofing)
- [Accessing The Current Route](#accessing-the-current-route)
- [Dependency injection](#dependency-injection)
- [Enabling dependency injection](#enabling-dependency-injection)
- [More reading](#more-reading)
- [Other examples](#other-examples)
- [CSRF-protection](#csrf-protection)
@@ -52,7 +55,7 @@ You can donate any amount of your choice by [clicking here](https://www.paypal.c
- [Custom Token-provider](#custom-token-provider)
- [Middlewares](#middlewares)
- [Example](#example)
- [Example](#example-1)
- [ExceptionHandlers](#exceptionhandlers)
- [Handling 404, 403 and other errors](#handling-404-403-and-other-errors)
@@ -674,6 +677,88 @@ SimpleRouter::request()->getLoadedRoute();
request()->getLoadedRoute();
```
## Dependency injection
simple-router supports dependency injection using the [`php-di`](http://php-di.org/) library.
Dependency injection allows the framework to automatically "inject" (load) classes added as parameters. This can simplify your code, as you can avoid creating new instances of objects you are using often in your `Controllers` etc.
Here's a basic example of a controller class using dependency injection:
```php
namespace Demo\Controllers;
class DefaultController {
public function login(User $user): string
{
// ...
}
}
```
The example above will automatically create a new instance of the `User` from the `$user` parameter. This means that the `$user` class contains a new instance of the `User` class and we won't need to create a new instance our self.
**WARNING:** dependency injection can have some negative impact in performance. If you experience any performance issues, we recommend disabling this functionality.
### Enabling dependency injection
Dependency injection is disabled per default to avoid any performance issues.
Before enabling dependency injection, we recommend that you read the [Container configuration](http://php-di.org/doc/container-configuration.html) section of the php-di documentation. This section covers how to configure php-di to different environments and speed-up the performance.
#### Enabling for development environment
The example below should ONLY be used on a development environment.
```php
// Create our new php-di container
$container = (new \DI\ContainerBuilder())
->useAutowiring(true)
->build();
// Add our container to simple-router and enable dependency injection
SimpleRouter::enableDependencyInjection($container);
```
Please check the [More reading](#more-reading) section of the documentation for useful php-di links and tutorials.
#### Enabling for production environment
The example below compiles the injections, which can help speed up performance.
**Note:** You should change the `$cacheDir` to a cache-storage within your project.
```php
// Cache directory
$cacheDir = sys_get_temp_dir('simple-router');
// Create our new php-di container
$container = (new \DI\ContainerBuilder())
->enableCompilation($cacheDir)
->writeProxiesToFile(true, $cacheDir . '/proxies')
->useAutowiring(true)
->build();
// Add our container to simple-router and enable dependency injection
SimpleRouter::enableDependencyInjection($container);
```
Please check the [More reading](#more-reading) section of the documentation for useful php-di links and tutorials.
### More reading
For more information about dependency injection, configuration and settings - we recommend that you check the php-di documentation or some of the useful links we've gathered below.
#### Useful links
- [php-di documentation](http://php-di.org/doc/)
- [Understanding dependency injection](http://php-di.org/doc/understanding-di.html)
- [Best practices guide](http://php-di.org/doc/best-practices.html)
- [Configuring the container](http://php-di.org/doc/container-configuration.html)
- [Definitions](http://php-di.org/doc/definition.html)
## Other examples
You can find many more examples in the `routes.php` example-file below:
+3 -2
View File
@@ -27,7 +27,8 @@
}
],
"require": {
"php": ">=7.1"
"php": ">=7.1",
"php-di/php-di": "^6.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
@@ -38,4 +39,4 @@
"Pecee\\": "src/Pecee/"
}
}
}
}
+2 -2
View File
@@ -214,7 +214,7 @@ class InputHandler
* @param array ...$methods
* @return IInputItem|null
*/
public function get(string $index, ...$methods) : ?IInputItem
public function get(string $index, ...$methods): ?IInputItem
{
$element = null;
@@ -241,7 +241,7 @@ class InputHandler
* @param array ...$methods
* @return string
*/
public function getValue(string $index, ?string $defaultValue = null, ...$methods) : ?string
public function getValue(string $index, ?string $defaultValue = null, ...$methods): ?string
{
$input = $this->get($index, $methods);
@@ -0,0 +1,118 @@
<?php
namespace Pecee\SimpleRouter\ClassLoader;
use DI\Container;
use Pecee\SimpleRouter\Exceptions\NotFoundHttpException;
class ClassLoader implements IClassLoader
{
/**
* Dependency injection enabled
* @var bool
*/
protected $useDependencyInjection = false;
/**
* @var Container|null
*/
protected $container;
/**
* Load class
*
* @param string $class
* @return mixed
* @throws NotFoundHttpException
*/
public function loadClass(string $class)
{
if (class_exists($class) === false) {
throw new NotFoundHttpException(sprintf('Class "%s" does not exist', $class), 404);
}
if ($this->useDependencyInjection === true) {
$container = $this->getContainer();
if ($container !== null) {
try {
return $container->get($class);
} catch (\Exception $e) {
throw new NotFoundHttpException($e->getMessage(), (int)$e->getCode(), $e->getPrevious());
}
}
}
return new $class();
}
/**
* Load closure
*
* @param \Closure $closure
* @param array $parameters
* @return mixed
* @throws NotFoundHttpException
*/
public function loadClosure(\Closure $closure, array $parameters)
{
if ($this->useDependencyInjection === true) {
$container = $this->getContainer();
if ($container !== null) {
try {
return $container->call($closure, $parameters);
} catch (\Exception $e) {
throw new NotFoundHttpException($e->getMessage(), (int)$e->getCode(), $e->getPrevious());
}
}
}
return \call_user_func_array($closure, $parameters);
}
/**
* Get dependency injector container.
*
* @return Container|null
*/
public function getContainer(): ?Container
{
return $this->container;
}
/**
* Set the dependency-injector container.
*
* @param Container $container
* @return ClassLoader
*/
public function setContainer(Container $container): self
{
$this->container = $container;
return $this;
}
/**
* Enable or disable dependency injection.
*
* @param bool $enabled
* @return static
*/
public function useDependencyInjection(bool $enabled): self
{
$this->useDependencyInjection = $enabled;
return $this;
}
/**
* Return true if dependency injection is enabled.
*
* @return bool
*/
public function isDependencyInjectionEnabled(): bool
{
return $this->useDependencyInjection;
}
}
@@ -0,0 +1,12 @@
<?php
namespace Pecee\SimpleRouter\ClassLoader;
interface IClassLoader
{
public function loadClass(string $class);
public function loadClosure(\Closure $closure, array $parameters);
}
@@ -150,7 +150,7 @@ class EventHandler implements IEventHandler
$events = [];
foreach ($names as $eventName) {
if(isset($this->registeredEvents[$eventName]) === true) {
if (isset($this->registeredEvents[$eventName]) === true) {
$events += $this->registeredEvents[$eventName];
}
}
@@ -4,7 +4,8 @@ namespace Pecee\SimpleRouter\Handlers;
use Pecee\SimpleRouter\Router;
interface IEventHandler {
interface IEventHandler
{
/**
* Get events.
@@ -12,7 +13,7 @@ interface IEventHandler {
* @param string|null $name Filter events by name.
* @return array
*/
public function getEvents(?string $name) : array;
public function getEvents(?string $name): array;
/**
* Fires any events registered with given event-name
@@ -21,6 +22,6 @@ interface IEventHandler {
* @param string $name Event name
* @param array $eventArgs Event arguments
*/
public function fireEvents(Router $router, string $name, array $eventArgs = []) : void;
public function fireEvents(Router $router, string $name, array $eventArgs = []): void;
}
+1 -1
View File
@@ -2,8 +2,8 @@
namespace Pecee\SimpleRouter\Route;
use Pecee\SimpleRouter\Handlers\IExceptionHandler;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Handlers\IExceptionHandler;
interface IGroupRoute extends IRoute
{
@@ -35,7 +35,7 @@ abstract class LoadableRoute extends Route implements ILoadableRoute
foreach ($this->getMiddlewares() as $middleware) {
if (\is_object($middleware) === false) {
$middleware = $this->loadClass($middleware);
$middleware = $router->getClassLoader()->loadClass($middleware);
}
if (($middleware instanceof IMiddleware) === false) {
+3 -17
View File
@@ -57,21 +57,6 @@ abstract class Route implements IRoute
protected $originalParameters = [];
protected $middlewares = [];
/**
* Load class by name
* @param string $name
* @return mixed
* @throws NotFoundHttpException
*/
protected function loadClass($name)
{
if (class_exists($name) === false) {
throw new NotFoundHttpException(sprintf('Class "%s" does not exist', $name), 404);
}
return new $name();
}
/**
* Render route
*
@@ -107,7 +92,7 @@ abstract class Route implements IRoute
/* When the callback is a function */
return \call_user_func_array($callback, $parameters);
return $router->getClassLoader()->loadClosure($callback, $parameters);
}
/* When the callback is a class + method */
@@ -118,7 +103,8 @@ abstract class Route implements IRoute
$className = ($namespace !== null && $controller[0][0] !== '\\') ? $namespace . '\\' . $controller[0] : $controller[0];
$router->debug('Loading class %s', $className);
$class = $this->loadClass($className);
$class = $router->getClassLoader()->loadClass($className);
$method = $controller[1];
if (method_exists($class, $method) === false) {
+1 -1
View File
@@ -2,8 +2,8 @@
namespace Pecee\SimpleRouter\Route;
use Pecee\SimpleRouter\Handlers\IExceptionHandler;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Handlers\IExceptionHandler;
class RouteGroup extends Route implements IGroupRoute
{
+58 -15
View File
@@ -4,14 +4,16 @@ namespace Pecee\SimpleRouter;
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\Http\Middleware\BaseCsrfVerifier;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Exceptions\HttpException;
use Pecee\SimpleRouter\Exceptions\NotFoundHttpException;
use Pecee\SimpleRouter\Route\IControllerRoute;
use Pecee\SimpleRouter\Route\IGroupRoute;
use Pecee\SimpleRouter\Route\ILoadableRoute;
@@ -102,6 +104,12 @@ class Router
*/
protected $eventHandlers = [];
/**
* Class loader instance
* @var ClassLoader
*/
protected $classLoader;
/**
* Router constructor.
*/
@@ -115,6 +123,7 @@ class Router
*/
public function reset(): void
{
$this->debugStartTime = microtime(true);
$this->isProcessingRoute = false;
try {
@@ -132,7 +141,7 @@ class Router
$this->eventHandlers = [];
$this->debugList = [];
$this->csrfVerifier = null;
$this->debugStartTime = microtime(true);
$this->classLoader = new ClassLoader();
}
/**
@@ -271,7 +280,7 @@ class Router
$this->debug('Rendering bootmanager "%s"', $className);
$this->fireEvents(EventHandler::EVENT_RENDER_BOOTMANAGER, [
'bootmanagers' => $this->bootManagers,
'bootmanager' => $manager,
'bootmanager' => $manager,
]);
/* Render bootmanager */
@@ -364,7 +373,7 @@ class Router
}
$this->fireEvents(EventHandler::EVENT_RENDER_MIDDLEWARES, [
'route' => $route,
'route' => $route,
'middlewares' => $route->getMiddlewares(),
]);
@@ -483,8 +492,8 @@ class Router
}
$this->fireEvents(EventHandler::EVENT_RENDER_EXCEPTION, [
'exception' => $e,
'exceptionHandler' => $handler,
'exception' => $e,
'exceptionHandler' => $handler,
'exceptionHandlers' => $this->exceptionHandlers,
]);
@@ -711,20 +720,28 @@ class Router
/**
* Set BootManagers
*
* @param array $bootManagers
* @return static
*/
public function setBootManagers(array $bootManagers): void
public function setBootManagers(array $bootManagers): self
{
$this->bootManagers = $bootManagers;
return $this;
}
/**
* Add BootManager
*
* @param IRouterBootManager $bootManager
* @return static
*/
public function addBootManager(IRouterBootManager $bootManager): void
public function addBootManager(IRouterBootManager $bootManager): self
{
$this->bootManagers[] = $bootManager;
return $this;
}
/**
@@ -783,13 +800,36 @@ class Router
* @param BaseCsrfVerifier $csrfVerifier
* @return static
*/
public function setCsrfVerifier(BaseCsrfVerifier $csrfVerifier)
public function setCsrfVerifier(BaseCsrfVerifier $csrfVerifier): self
{
$this->csrfVerifier = $csrfVerifier;
return $this;
}
/**
* Set class loader
*
* @param IClassLoader $loader
* @return static
*/
public function setClassLoader(IClassLoader $loader)
{
$this->classLoader = $loader;
return $this;
}
/**
* Get class loader
*
* @return ClassLoader
*/
public function getClassLoader(): IClassLoader
{
return $this->classLoader;
}
/**
* Register event handler
*
@@ -853,11 +893,14 @@ class Router
/**
* Enable or disables debugging
*
* @param bool $boolean
* @param bool $enabled
* @return static
*/
public function setDebugEnabled(bool $boolean): void
public function setDebugEnabled(bool $enabled): self
{
$this->debugEnabled = $boolean;
$this->debugEnabled = $enabled;
return $this;
}
/**
+18 -2
View File
@@ -10,14 +10,16 @@
namespace Pecee\SimpleRouter;
use DI\Container;
use Pecee\Exceptions\InvalidArgumentException;
use Pecee\Http\Exceptions\MalformedUrlException;
use Pecee\Http\Url;
use Pecee\SimpleRouter\Handlers\CallbackExceptionHandler;
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\IPartialGroupRoute;
@@ -530,6 +532,20 @@ class SimpleRouter
return $route;
}
/**
* Enable or disable dependency injection
*
* @param Container $container
* @return IClassLoader
*/
public static function enableDependencyInjection(Container $container): IClassLoader
{
return static::router()
->getClassLoader()
->useDependencyInjection(true)
->setContainer($container);
}
/**
* Get default namespace
* @return string|null
@@ -0,0 +1,53 @@
<?php
require_once 'Dummy/DummyMiddleware.php';
class DependencyInjectionTest extends \PHPUnit\Framework\TestCase
{
public function testDependencyInjectionDevelopment()
{
$builder = new \DI\ContainerBuilder();
$container = $builder
->useAutowiring(true)
->ignorePhpDocErrors(true)
->build();
TestRouter::enableDependencyInjection($container);
$className = null;
TestRouter::get('/', function (DummyMiddleware $url) use (&$className) {
$className = \get_class($url);
});
TestRouter::debug('/');
$this->assertEquals(DummyMiddleware::class, $className);
}
public function testDependencyInjectionProduction()
{
$cacheDir = dirname(__DIR__, 2) . '/tmp';
$builder = new \DI\ContainerBuilder();
$builder
->enableCompilation($cacheDir)
->writeProxiesToFile(true, $cacheDir . '/proxies')
->ignorePhpDocErrors(true)
->useAutowiring(true);
$container = $builder->build();
TestRouter::enableDependencyInjection($container);
$className = null;
TestRouter::get('/', function (DummyMiddleware $url) use (&$className) {
$className = \get_class($url);
});
TestRouter::debug('/');
$this->assertEquals(DummyMiddleware::class, $className);
}
}
+4 -5
View File
@@ -5,14 +5,13 @@ require_once 'Dummy/DummyController.php';
class GroupTest extends \PHPUnit\Framework\TestCase
{
protected $result;
public function testGroupLoad()
{
$this->result = false;
$result = false;
TestRouter::group(['prefix' => '/group'], function () {
$this->result = true;
TestRouter::group(['prefix' => '/group'], function () use(&$result) {
$result = true;
});
try {
@@ -20,7 +19,7 @@ class GroupTest extends \PHPUnit\Framework\TestCase
} catch(\Exception $e) {
}
$this->assertTrue($this->result);
$this->assertTrue($result);
}
public function testNestedGroup()
+14 -15
View File
@@ -6,21 +6,20 @@ require_once 'Dummy/Exception/ExceptionHandlerException.php';
class RouterRouteTest extends \PHPUnit\Framework\TestCase
{
protected $result = false;
public function testMultiParam()
{
TestRouter::get('/test-{param1}-{param2}', function ($param1, $param2) {
$result = false;
TestRouter::get('/test-{param1}-{param2}', function ($param1, $param2) use(&$result) {
if ($param1 === 'param1' && $param2 === 'param2') {
$this->result = true;
$result = true;
}
});
TestRouter::debug('/test-param1-param2', 'get');
$this->assertTrue($this->result);
$this->assertTrue($result);
}
@@ -92,19 +91,19 @@ class RouterRouteTest extends \PHPUnit\Framework\TestCase
public function testDomainAllowedRoute()
{
$this->result = false;
$result = false;
TestRouter::request()->setHost('hello.world.com');
TestRouter::group(['domain' => '{subdomain}.world.com'], function () {
TestRouter::get('/test', function ($subdomain = null) {
$this->result = ($subdomain === 'hello');
TestRouter::group(['domain' => '{subdomain}.world.com'], function () use(&$result) {
TestRouter::get('/test', function ($subdomain = null) use(&$result) {
$result = ($subdomain === 'hello');
});
});
TestRouter::debug('/test', 'get');
$this->assertTrue($this->result);
$this->assertTrue($result);
}
@@ -112,17 +111,17 @@ class RouterRouteTest extends \PHPUnit\Framework\TestCase
{
TestRouter::request()->setHost('other.world.com');
$this->result = false;
$result = false;
TestRouter::group(['domain' => '{subdomain}.world.com'], function () {
TestRouter::get('/test', function ($subdomain = null) {
$this->result = ($subdomain === 'hello');
TestRouter::group(['domain' => '{subdomain}.world.com'], function () use(&$result) {
TestRouter::get('/test', function ($subdomain = null) use(&$result) {
$result = ($subdomain === 'hello');
});
});
TestRouter::debug('/test', 'get');
$this->assertFalse($this->result);
$this->assertFalse($result);
}