Skip to content

Commit 4f98216

Browse files
committed
Merge pull request #16 from felixfbecker/master
Allow arbitrary injection into middleware and param converters
2 parents f53a571 + c22695e commit 4f98216

8 files changed

+498
-3
lines changed

README.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ $app->run();
3333

3434
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, …).
3535

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

3838
```php
3939
class Mailer
@@ -51,9 +51,21 @@ $app->post('/register/{name}', function ($name, Mailer $mailer) {
5151
$app->post('/register/{name}', function (Request $request, Mailer $mailer) {
5252
// ...
5353
});
54+
55+
// Injection works for middleware too
56+
$app->before(function (Request $request, Mailer $mailer) {
57+
// ...
58+
});
59+
60+
// And param converters
61+
$app->get('/users/{user}', function (User $user) {
62+
return new JsonResponse($user);
63+
})->convert('user', function ($user, UserManager $userManager) {
64+
return $userManager->findById($user);
65+
});
5466
```
5567

56-
Dependency injection in controllers works using type-hinting:
68+
Dependency injection works using type-hinting:
5769

5870
- it can be mixed with request parameters (`$name` in the example above)
5971
- the order of parameters doesn't matter, they are resolved by type-hint (for dependency injection) and by name (for request attributes)

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"require": {
1717
"php": ">=5.4",
1818
"php-di/php-di": "~5.0",
19-
"php-di/invoker": "~1.2",
19+
"php-di/invoker": "~1.3",
2020
"silex/silex" : "~1.3",
2121
"pimple/pimple" : "~1.1"
2222
},

src/Application.php

+126
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,24 @@
44

55
use DI\Bridge\Silex\Container\ContainerInteropProxy;
66
use DI\Bridge\Silex\Controller\ControllerResolver;
7+
use DI\Bridge\Silex\MiddlewareListener;
8+
use DI\Bridge\Silex\ConverterListener;
9+
use Silex\EventListener\LocaleListener;
10+
use Silex\EventListener\StringToResponseListener;
11+
use Silex\LazyUrlMatcher;
12+
use Symfony\Component\HttpKernel\Kernel;
13+
use Symfony\Component\HttpKernel\HttpKernelInterface;
14+
use Symfony\Component\HttpKernel\KernelEvents;
15+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
16+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
17+
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
18+
use Symfony\Component\HttpKernel\EventListener\ResponseListener;
19+
use Symfony\Component\HttpKernel\EventListener\RouterListener;
720
use DI\Container;
821
use DI\ContainerBuilder;
922
use Interop\Container\ContainerInterface;
1023
use Invoker\CallableResolver;
24+
use Invoker\Reflection\CallableReflection;
1125
use Invoker\ParameterResolver\AssociativeArrayResolver;
1226
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
1327
use Invoker\ParameterResolver\ResolverChain;
@@ -29,6 +43,11 @@ class Application extends \Silex\Application
2943
*/
3044
private $phpdi;
3145

46+
/**
47+
* @var CallbackInvoker
48+
*/
49+
private $callbackInvoker;
50+
3251
/**
3352
* @param ContainerBuilder|null $containerBuilder You can optionally provide your preconfigured container builder.
3453
* @param array $values
@@ -45,6 +64,7 @@ public function __construct(ContainerBuilder $containerBuilder = null, array $va
4564
]);
4665
$containerBuilder->wrapContainer($this->containerInteropProxy);
4766
$this->phpdi = $containerBuilder->build();
67+
$this->callbackInvoker = new CallbackInvoker($this->containerInteropProxy);
4868

4969
parent::__construct($values);
5070

@@ -70,6 +90,33 @@ public function __construct(ContainerBuilder $containerBuilder = null, array $va
7090
$this['phpdi.callable_resolver']
7191
);
7292
});
93+
94+
// Override the dispatcher with ours to use our event listeners
95+
$this['dispatcher'] = $this->share(function () {
96+
/**
97+
* @var EventDispatcherInterface
98+
*/
99+
$dispatcher = new $this['dispatcher_class']();
100+
101+
$urlMatcher = new LazyUrlMatcher(function () {
102+
return $this['url_matcher'];
103+
});
104+
if (Kernel::VERSION_ID >= 20800) {
105+
$dispatcher->addSubscriber(new RouterListener($urlMatcher, $this['request_stack'], $this['request_context'], $this['logger']));
106+
} else {
107+
$dispatcher->addSubscriber(new RouterListener($urlMatcher, $this['request_context'], $this['logger'], $this['request_stack']));
108+
}
109+
$dispatcher->addSubscriber(new LocaleListener($this, $urlMatcher, $this['request_stack']));
110+
if (isset($this['exception_handler'])) {
111+
$dispatcher->addSubscriber($this['exception_handler']);
112+
}
113+
$dispatcher->addSubscriber(new ResponseListener($this['charset']));
114+
$dispatcher->addSubscriber(new MiddlewareListener($this, $this->callbackInvoker));
115+
$dispatcher->addSubscriber(new ConverterListener($this['routes'], $this['callback_resolver'], $this->callbackInvoker));
116+
$dispatcher->addSubscriber(new StringToResponseListener());
117+
118+
return $dispatcher;
119+
});
73120
}
74121

75122
public function offsetGet($id)
@@ -100,4 +147,83 @@ public function getPhpDi()
100147
{
101148
return $this->phpdi;
102149
}
150+
151+
public function before($callback, $priority = 0)
152+
{
153+
$this->on(KernelEvents::REQUEST, function (GetResponseEvent $event) use ($callback) {
154+
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
155+
return;
156+
}
157+
158+
$request = $event->getRequest();
159+
$middleware = $this['callback_resolver']->resolveCallback($callback);
160+
$ret = $this->callbackInvoker->call($middleware, [
161+
// type hints
162+
'Symfony\Component\HttpFoundation\Request' => $request,
163+
// Silex' default parameter order
164+
0 => $request,
165+
1 => $this,
166+
]);
167+
168+
if ($ret instanceof Response) {
169+
$event->setResponse($ret);
170+
}
171+
172+
}, $priority);
173+
}
174+
175+
public function after($callback, $priority = 0)
176+
{
177+
$this->on(KernelEvents::RESPONSE, function (FilterResponseEvent $event) use ($callback) {
178+
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
179+
return;
180+
}
181+
182+
$request = $event->getRequest();
183+
$response = $event->getResponse();
184+
$middleware = $this['callback_resolver']->resolveCallback($callback);
185+
$ret = $this->callbackInvoker->call($middleware, [
186+
// type hints
187+
'Symfony\Component\HttpFoundation\Request' => $request,
188+
'Symfony\Component\HttpFoundation\Response' => $response,
189+
// Silex' default parameter order
190+
0 => $request,
191+
1 => $response,
192+
2 => $this,
193+
]);
194+
195+
if ($ret instanceof Response) {
196+
$event->setResponse($ret);
197+
} elseif (null !== $ret) {
198+
throw new \RuntimeException('An after middleware returned an invalid response value. Must return null or an instance of Response.');
199+
}
200+
201+
}, $priority);
202+
}
203+
204+
public function finish($callback, $priority = 0)
205+
{
206+
$this->on(KernelEvents::TERMINATE, function (PostResponseEvent $event) use ($callback) {
207+
208+
$request = $event->getRequest();
209+
$response = $event->getResponse();
210+
$middleware = $this['callback_resolver']->resolveCallback($callback);
211+
$ret = $this->callbackInvoker->call($middleware, [
212+
// type hints
213+
'Symfony\Component\HttpFoundation\Request' => $request,
214+
'Symfony\Component\HttpFoundation\Response' => $response,
215+
// Silex' default parameter order
216+
0 => $request,
217+
1 => $response,
218+
2 => $this,
219+
]);
220+
221+
if ($ret instanceof Response) {
222+
$event->setResponse($ret);
223+
} elseif (null !== $ret) {
224+
throw new \RuntimeException('An after middleware returned an invalid response value. Must return null or an instance of Response.');
225+
}
226+
227+
}, $priority);
228+
}
103229
}

src/CallbackInvoker.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace DI\Bridge\Silex;
4+
5+
use DI\Bridge\Silex\Application;
6+
use Invoker\Invoker;
7+
use Interop\Container\ContainerInterface;
8+
use Invoker\ParameterResolver\ResolverChain;
9+
use Invoker\ParameterResolver\TypeHintResolver;
10+
use Invoker\ParameterResolver\AssociativeArrayResolver;
11+
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
12+
use Invoker\ParameterResolver\NumericArrayResolver;
13+
14+
/**
15+
* A subclass of Invoker that always tries to first resolve through provided parameter names, then
16+
* type hints, then through the DI container and finally allows a fallback to a default parameter order
17+
*
18+
* @author Felix Becker <[email protected]>
19+
*/
20+
class CallbackInvoker extends Invoker
21+
{
22+
/**
23+
* @param ContainerInterface $container the container for injection
24+
*/
25+
public function __construct(ContainerInterface $container)
26+
{
27+
parent::__construct(new ResolverChain([
28+
new AssociativeArrayResolver,
29+
new TypeHintResolver,
30+
new TypeHintContainerResolver($container),
31+
new NumericArrayResolver,
32+
]));
33+
}
34+
}

src/ConverterListener.php

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace DI\Bridge\Silex;
4+
5+
use DI\Bridge\Silex\CallbackResolver;
6+
use Interop\Container\ContainerInterface;
7+
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
8+
use Symfony\Component\Routing\RouteCollection;
9+
10+
/**
11+
* Replacement for the Silex ConverterListener to allow arbitrary injection into param converters.
12+
*
13+
* @author Felix Becker <[email protected]>
14+
*/
15+
class ConverterListener extends \Silex\EventListener\ConverterListener
16+
{
17+
/**
18+
* @var CallbackInvoker
19+
*/
20+
private $callbackInvoker;
21+
22+
/**
23+
* @param RouteCollection $routes A RouteCollection instance
24+
* @param CallbackResolver $callbackResolver A CallbackResolver instance
25+
* @param CallbackInvoker $callbackInvoker The invoker that handles resolving and injecting param converters
26+
*/
27+
public function __construct(RouteCollection $routes, CallbackResolver $callbackResolver, CallbackInvoker $callbackInvoker)
28+
{
29+
parent::__construct($routes, $callbackResolver);
30+
$this->callbackInvoker = $callbackInvoker;
31+
}
32+
33+
public function onKernelController(FilterControllerEvent $event)
34+
{
35+
$request = $event->getRequest();
36+
$route = $this->routes->get($request->attributes->get('_route'));
37+
if ($route && $converters = $route->getOption('_converters')) {
38+
foreach ($converters as $name => $callback) {
39+
40+
$value = $request->attributes->get($name);
41+
$middleware = $this->callbackResolver->resolveCallback($callback);
42+
$ret = $this->callbackInvoker->call($middleware, [
43+
// parameter name
44+
$name => $value,
45+
// type hints
46+
'Symfony\Component\HttpFoundation\Request' => $request,
47+
// Silex' default parameter order
48+
0 => $value,
49+
1 => $request,
50+
]);
51+
52+
$request->attributes->set($name, $ret);
53+
}
54+
}
55+
}
56+
}

src/MiddlewareListener.php

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace DI\Bridge\Silex;
4+
5+
use DI\Bridge\Silex\Application;
6+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
7+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
/**
11+
* Replacement for the Silex MiddlewareListener to allow arbitrary injection into middleware functions.
12+
*
13+
* @author Felix Becker <[email protected]>
14+
*/
15+
class MiddlewareListener extends \Silex\EventListener\MiddlewareListener
16+
{
17+
/**
18+
* @var CallbackInvoker
19+
*/
20+
private $callbackInvoker;
21+
22+
/**
23+
* @param Application $app The application
24+
* @param CallbackInvoker $callbackInvoker The invoker that handles injecting middlewares
25+
*/
26+
public function __construct(Application $app, CallbackInvoker $callbackInvoker)
27+
{
28+
parent::__construct($app);
29+
$this->callbackInvoker = $callbackInvoker;
30+
}
31+
32+
public function onKernelRequest(GetResponseEvent $event)
33+
{
34+
$request = $event->getRequest();
35+
$routeName = $request->attributes->get('_route');
36+
if (!$route = $this->app['routes']->get($routeName)) {
37+
return;
38+
}
39+
40+
foreach ((array) $route->getOption('_before_middlewares') as $callback) {
41+
42+
$middleware = $this->app['callback_resolver']->resolveCallback($callback);
43+
$ret = $this->callbackInvoker->call($middleware, [
44+
// type hints
45+
'Symfony\Component\HttpFoundation\Request' => $request,
46+
// Silex' default parameter order
47+
0 => $request,
48+
1 => $this->app,
49+
]);
50+
51+
if ($ret instanceof Response) {
52+
$event->setResponse($ret);
53+
} elseif (null !== $ret) {
54+
throw new \RuntimeException(sprintf('A before middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName));
55+
}
56+
}
57+
}
58+
59+
public function onKernelResponse(FilterResponseEvent $event)
60+
{
61+
$request = $event->getRequest();
62+
$response = $event->getResponse();
63+
$routeName = $request->attributes->get('_route');
64+
if (!$route = $this->app['routes']->get($routeName)) {
65+
return;
66+
}
67+
68+
foreach ((array) $route->getOption('_after_middlewares') as $callback) {
69+
70+
$middleware = $this->app['callback_resolver']->resolveCallback($callback);
71+
$ret = $this->callbackInvoker->call($middleware, [
72+
// type hints
73+
'Symfony\Component\HttpFoundation\Request' => $request,
74+
'Symfony\Component\HttpFoundation\Response' => $response,
75+
// Silex' default parameter order
76+
0 => $request,
77+
1 => $response,
78+
2 => $this->app,
79+
]);
80+
81+
if ($ret instanceof Response) {
82+
$event->setResponse($ret);
83+
} elseif (null !== $ret) {
84+
throw new \RuntimeException(sprintf('An after middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName));
85+
}
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)