Skip to content

Commit

Permalink
Merge pull request #16 from felixfbecker/master
Browse files Browse the repository at this point in the history
Allow arbitrary injection into middleware and param converters
  • Loading branch information
mnapoli committed Mar 21, 2016
2 parents f53a571 + c22695e commit 4f98216
Show file tree
Hide file tree
Showing 8 changed files with 498 additions and 3 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ $app->run();

Using PHP-DI in Silex allows you to use all the awesome features of PHP-DI to wire your dependencies (using the definition files, autowiring, annotations, …).

Another big benefit of the PHP-DI integration is the ability to use dependency injection inside controllers:
Another big benefit of the PHP-DI integration is the ability to use dependency injection inside controllers, middlewares and param converters:

```php
class Mailer
Expand All @@ -51,9 +51,21 @@ $app->post('/register/{name}', function ($name, Mailer $mailer) {
$app->post('/register/{name}', function (Request $request, Mailer $mailer) {
// ...
});

// Injection works for middleware too
$app->before(function (Request $request, Mailer $mailer) {
// ...
});

// And param converters
$app->get('/users/{user}', function (User $user) {
return new JsonResponse($user);
})->convert('user', function ($user, UserManager $userManager) {
return $userManager->findById($user);
});
```

Dependency injection in controllers works using type-hinting:
Dependency injection works using type-hinting:

- it can be mixed with request parameters (`$name` in the example above)
- the order of parameters doesn't matter, they are resolved by type-hint (for dependency injection) and by name (for request attributes)
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"require": {
"php": ">=5.4",
"php-di/php-di": "~5.0",
"php-di/invoker": "~1.2",
"php-di/invoker": "~1.3",
"silex/silex" : "~1.3",
"pimple/pimple" : "~1.1"
},
Expand Down
126 changes: 126 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,24 @@

use DI\Bridge\Silex\Container\ContainerInteropProxy;
use DI\Bridge\Silex\Controller\ControllerResolver;
use DI\Bridge\Silex\MiddlewareListener;
use DI\Bridge\Silex\ConverterListener;
use Silex\EventListener\LocaleListener;
use Silex\EventListener\StringToResponseListener;
use Silex\LazyUrlMatcher;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\EventListener\ResponseListener;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use DI\Container;
use DI\ContainerBuilder;
use Interop\Container\ContainerInterface;
use Invoker\CallableResolver;
use Invoker\Reflection\CallableReflection;
use Invoker\ParameterResolver\AssociativeArrayResolver;
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
use Invoker\ParameterResolver\ResolverChain;
Expand All @@ -29,6 +43,11 @@ class Application extends \Silex\Application
*/
private $phpdi;

/**
* @var CallbackInvoker
*/
private $callbackInvoker;

/**
* @param ContainerBuilder|null $containerBuilder You can optionally provide your preconfigured container builder.
* @param array $values
Expand All @@ -45,6 +64,7 @@ public function __construct(ContainerBuilder $containerBuilder = null, array $va
]);
$containerBuilder->wrapContainer($this->containerInteropProxy);
$this->phpdi = $containerBuilder->build();
$this->callbackInvoker = new CallbackInvoker($this->containerInteropProxy);

parent::__construct($values);

Expand All @@ -70,6 +90,33 @@ public function __construct(ContainerBuilder $containerBuilder = null, array $va
$this['phpdi.callable_resolver']
);
});

// Override the dispatcher with ours to use our event listeners
$this['dispatcher'] = $this->share(function () {
/**
* @var EventDispatcherInterface
*/
$dispatcher = new $this['dispatcher_class']();

$urlMatcher = new LazyUrlMatcher(function () {
return $this['url_matcher'];
});
if (Kernel::VERSION_ID >= 20800) {
$dispatcher->addSubscriber(new RouterListener($urlMatcher, $this['request_stack'], $this['request_context'], $this['logger']));
} else {
$dispatcher->addSubscriber(new RouterListener($urlMatcher, $this['request_context'], $this['logger'], $this['request_stack']));
}
$dispatcher->addSubscriber(new LocaleListener($this, $urlMatcher, $this['request_stack']));
if (isset($this['exception_handler'])) {
$dispatcher->addSubscriber($this['exception_handler']);
}
$dispatcher->addSubscriber(new ResponseListener($this['charset']));
$dispatcher->addSubscriber(new MiddlewareListener($this, $this->callbackInvoker));
$dispatcher->addSubscriber(new ConverterListener($this['routes'], $this['callback_resolver'], $this->callbackInvoker));
$dispatcher->addSubscriber(new StringToResponseListener());

return $dispatcher;
});
}

public function offsetGet($id)
Expand Down Expand Up @@ -100,4 +147,83 @@ public function getPhpDi()
{
return $this->phpdi;
}

public function before($callback, $priority = 0)
{
$this->on(KernelEvents::REQUEST, function (GetResponseEvent $event) use ($callback) {
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}

$request = $event->getRequest();
$middleware = $this['callback_resolver']->resolveCallback($callback);
$ret = $this->callbackInvoker->call($middleware, [
// type hints
'Symfony\Component\HttpFoundation\Request' => $request,
// Silex' default parameter order
0 => $request,
1 => $this,
]);

if ($ret instanceof Response) {
$event->setResponse($ret);
}

}, $priority);
}

public function after($callback, $priority = 0)
{
$this->on(KernelEvents::RESPONSE, function (FilterResponseEvent $event) use ($callback) {
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}

$request = $event->getRequest();
$response = $event->getResponse();
$middleware = $this['callback_resolver']->resolveCallback($callback);
$ret = $this->callbackInvoker->call($middleware, [
// type hints
'Symfony\Component\HttpFoundation\Request' => $request,
'Symfony\Component\HttpFoundation\Response' => $response,
// Silex' default parameter order
0 => $request,
1 => $response,
2 => $this,
]);

if ($ret instanceof Response) {
$event->setResponse($ret);
} elseif (null !== $ret) {
throw new \RuntimeException('An after middleware returned an invalid response value. Must return null or an instance of Response.');
}

}, $priority);
}

public function finish($callback, $priority = 0)
{
$this->on(KernelEvents::TERMINATE, function (PostResponseEvent $event) use ($callback) {

$request = $event->getRequest();
$response = $event->getResponse();
$middleware = $this['callback_resolver']->resolveCallback($callback);
$ret = $this->callbackInvoker->call($middleware, [
// type hints
'Symfony\Component\HttpFoundation\Request' => $request,
'Symfony\Component\HttpFoundation\Response' => $response,
// Silex' default parameter order
0 => $request,
1 => $response,
2 => $this,
]);

if ($ret instanceof Response) {
$event->setResponse($ret);
} elseif (null !== $ret) {
throw new \RuntimeException('An after middleware returned an invalid response value. Must return null or an instance of Response.');
}

}, $priority);
}
}
34 changes: 34 additions & 0 deletions src/CallbackInvoker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace DI\Bridge\Silex;

use DI\Bridge\Silex\Application;
use Invoker\Invoker;
use Interop\Container\ContainerInterface;
use Invoker\ParameterResolver\ResolverChain;
use Invoker\ParameterResolver\TypeHintResolver;
use Invoker\ParameterResolver\AssociativeArrayResolver;
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
use Invoker\ParameterResolver\NumericArrayResolver;

/**
* A subclass of Invoker that always tries to first resolve through provided parameter names, then
* type hints, then through the DI container and finally allows a fallback to a default parameter order
*
* @author Felix Becker <[email protected]>
*/
class CallbackInvoker extends Invoker
{
/**
* @param ContainerInterface $container the container for injection
*/
public function __construct(ContainerInterface $container)
{
parent::__construct(new ResolverChain([
new AssociativeArrayResolver,
new TypeHintResolver,
new TypeHintContainerResolver($container),
new NumericArrayResolver,
]));
}
}
56 changes: 56 additions & 0 deletions src/ConverterListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace DI\Bridge\Silex;

use DI\Bridge\Silex\CallbackResolver;
use Interop\Container\ContainerInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Routing\RouteCollection;

/**
* Replacement for the Silex ConverterListener to allow arbitrary injection into param converters.
*
* @author Felix Becker <[email protected]>
*/
class ConverterListener extends \Silex\EventListener\ConverterListener
{
/**
* @var CallbackInvoker
*/
private $callbackInvoker;

/**
* @param RouteCollection $routes A RouteCollection instance
* @param CallbackResolver $callbackResolver A CallbackResolver instance
* @param CallbackInvoker $callbackInvoker The invoker that handles resolving and injecting param converters
*/
public function __construct(RouteCollection $routes, CallbackResolver $callbackResolver, CallbackInvoker $callbackInvoker)
{
parent::__construct($routes, $callbackResolver);
$this->callbackInvoker = $callbackInvoker;
}

public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
$route = $this->routes->get($request->attributes->get('_route'));
if ($route && $converters = $route->getOption('_converters')) {
foreach ($converters as $name => $callback) {

$value = $request->attributes->get($name);
$middleware = $this->callbackResolver->resolveCallback($callback);
$ret = $this->callbackInvoker->call($middleware, [
// parameter name
$name => $value,
// type hints
'Symfony\Component\HttpFoundation\Request' => $request,
// Silex' default parameter order
0 => $value,
1 => $request,
]);

$request->attributes->set($name, $ret);
}
}
}
}
88 changes: 88 additions & 0 deletions src/MiddlewareListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace DI\Bridge\Silex;

use DI\Bridge\Silex\Application;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpFoundation\Response;

/**
* Replacement for the Silex MiddlewareListener to allow arbitrary injection into middleware functions.
*
* @author Felix Becker <[email protected]>
*/
class MiddlewareListener extends \Silex\EventListener\MiddlewareListener
{
/**
* @var CallbackInvoker
*/
private $callbackInvoker;

/**
* @param Application $app The application
* @param CallbackInvoker $callbackInvoker The invoker that handles injecting middlewares
*/
public function __construct(Application $app, CallbackInvoker $callbackInvoker)
{
parent::__construct($app);
$this->callbackInvoker = $callbackInvoker;
}

public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$routeName = $request->attributes->get('_route');
if (!$route = $this->app['routes']->get($routeName)) {
return;
}

foreach ((array) $route->getOption('_before_middlewares') as $callback) {

$middleware = $this->app['callback_resolver']->resolveCallback($callback);
$ret = $this->callbackInvoker->call($middleware, [
// type hints
'Symfony\Component\HttpFoundation\Request' => $request,
// Silex' default parameter order
0 => $request,
1 => $this->app,
]);

if ($ret instanceof Response) {
$event->setResponse($ret);
} elseif (null !== $ret) {
throw new \RuntimeException(sprintf('A before middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName));
}
}
}

public function onKernelResponse(FilterResponseEvent $event)
{
$request = $event->getRequest();
$response = $event->getResponse();
$routeName = $request->attributes->get('_route');
if (!$route = $this->app['routes']->get($routeName)) {
return;
}

foreach ((array) $route->getOption('_after_middlewares') as $callback) {

$middleware = $this->app['callback_resolver']->resolveCallback($callback);
$ret = $this->callbackInvoker->call($middleware, [
// type hints
'Symfony\Component\HttpFoundation\Request' => $request,
'Symfony\Component\HttpFoundation\Response' => $response,
// Silex' default parameter order
0 => $request,
1 => $response,
2 => $this->app,
]);

if ($ret instanceof Response) {
$event->setResponse($ret);
} elseif (null !== $ret) {
throw new \RuntimeException(sprintf('An after middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName));
}
}
}
}
Loading

0 comments on commit 4f98216

Please sign in to comment.