diff --git a/.gitignore b/.gitignore index 848a58c..4d58d1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea composer.lock -vendor/ \ No newline at end of file +vendor/ +tests/tmp/* \ No newline at end of file diff --git a/README.md b/README.md index 0850b38..80c329a 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/composer.json b/composer.json index 9c88dd6..d0af205 100644 --- a/composer.json +++ b/composer.json @@ -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/" } } -} \ No newline at end of file +} diff --git a/src/Pecee/Http/Input/InputHandler.php b/src/Pecee/Http/Input/InputHandler.php index 1f1d0e1..9b15a2a 100644 --- a/src/Pecee/Http/Input/InputHandler.php +++ b/src/Pecee/Http/Input/InputHandler.php @@ -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); diff --git a/src/Pecee/SimpleRouter/ClassLoader/ClassLoader.php b/src/Pecee/SimpleRouter/ClassLoader/ClassLoader.php new file mode 100644 index 0000000..caea1a5 --- /dev/null +++ b/src/Pecee/SimpleRouter/ClassLoader/ClassLoader.php @@ -0,0 +1,118 @@ +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; + } + +} \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/ClassLoader/IClassLoader.php b/src/Pecee/SimpleRouter/ClassLoader/IClassLoader.php new file mode 100644 index 0000000..d978ac1 --- /dev/null +++ b/src/Pecee/SimpleRouter/ClassLoader/IClassLoader.php @@ -0,0 +1,12 @@ +registeredEvents[$eventName]) === true) { + if (isset($this->registeredEvents[$eventName]) === true) { $events += $this->registeredEvents[$eventName]; } } diff --git a/src/Pecee/SimpleRouter/Handlers/IEventHandler.php b/src/Pecee/SimpleRouter/Handlers/IEventHandler.php index 9ae63c2..bf4e49a 100644 --- a/src/Pecee/SimpleRouter/Handlers/IEventHandler.php +++ b/src/Pecee/SimpleRouter/Handlers/IEventHandler.php @@ -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; } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/Route/IGroupRoute.php b/src/Pecee/SimpleRouter/Route/IGroupRoute.php index a1ebd33..2c18d1d 100644 --- a/src/Pecee/SimpleRouter/Route/IGroupRoute.php +++ b/src/Pecee/SimpleRouter/Route/IGroupRoute.php @@ -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 { diff --git a/src/Pecee/SimpleRouter/Route/LoadableRoute.php b/src/Pecee/SimpleRouter/Route/LoadableRoute.php index d76cee7..9fc4cb3 100644 --- a/src/Pecee/SimpleRouter/Route/LoadableRoute.php +++ b/src/Pecee/SimpleRouter/Route/LoadableRoute.php @@ -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) { diff --git a/src/Pecee/SimpleRouter/Route/Route.php b/src/Pecee/SimpleRouter/Route/Route.php index 0426dfb..ad56adc 100644 --- a/src/Pecee/SimpleRouter/Route/Route.php +++ b/src/Pecee/SimpleRouter/Route/Route.php @@ -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) { diff --git a/src/Pecee/SimpleRouter/Route/RouteGroup.php b/src/Pecee/SimpleRouter/Route/RouteGroup.php index 3ad9060..4e72215 100644 --- a/src/Pecee/SimpleRouter/Route/RouteGroup.php +++ b/src/Pecee/SimpleRouter/Route/RouteGroup.php @@ -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 { diff --git a/src/Pecee/SimpleRouter/Router.php b/src/Pecee/SimpleRouter/Router.php index 425a44d..05d9908 100644 --- a/src/Pecee/SimpleRouter/Router.php +++ b/src/Pecee/SimpleRouter/Router.php @@ -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; } /** diff --git a/src/Pecee/SimpleRouter/SimpleRouter.php b/src/Pecee/SimpleRouter/SimpleRouter.php index 8e56f2d..3209812 100644 --- a/src/Pecee/SimpleRouter/SimpleRouter.php +++ b/src/Pecee/SimpleRouter/SimpleRouter.php @@ -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 diff --git a/tests/Pecee/SimpleRouter/DependencyInjectionTest.php b/tests/Pecee/SimpleRouter/DependencyInjectionTest.php new file mode 100644 index 0000000..8863f19 --- /dev/null +++ b/tests/Pecee/SimpleRouter/DependencyInjectionTest.php @@ -0,0 +1,53 @@ +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); + } +} \ No newline at end of file diff --git a/tests/Pecee/SimpleRouter/GroupTest.php b/tests/Pecee/SimpleRouter/GroupTest.php index c059253..ce79b26 100644 --- a/tests/Pecee/SimpleRouter/GroupTest.php +++ b/tests/Pecee/SimpleRouter/GroupTest.php @@ -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() diff --git a/tests/Pecee/SimpleRouter/RouterRouteTest.php b/tests/Pecee/SimpleRouter/RouterRouteTest.php index cfdd490..8e3caea 100644 --- a/tests/Pecee/SimpleRouter/RouterRouteTest.php +++ b/tests/Pecee/SimpleRouter/RouterRouteTest.php @@ -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); }