PHP-DI / Silex-Bridge

PHP-DI integration in Silex
http://php-di.org/doc/frameworks/silex.html
MIT License
24 stars 8 forks source link

Allow arbitrary injection into middleware and param converters #16

Closed felixfbecker closed 8 years ago

felixfbecker commented 8 years ago

This is an implementation for #15 I would love to get this merged as quickly as possible to depend on it in my projects.


Feature

It is now possible to inject whatever is registered in the container into route param converters and middleware via type hints. Also, as long as the arguments are type-hinted, argument position of request / response / app doesn't matter anymore. If there are no type-hints, it falls back to Silex' default argument order.

Implementation

To accomplish this, ConverterListener and MiddlewareListener are overwritten and in Application the default dispatcher is overwritten to use the custom classes. For application middleware, before/after/finish are overwritten aswell.

For DRY reasons the injection logic is done in separate classes (BeforeMiddlewareCaller, AfterMiddlewareCaller, ConverterCaller). This allows the application middleware and route middleware to share the same logic and also for all event listeners to share the same instance of the caller and ParameterResolver objects.

Tests

ConverterTest and MiddlewareTest test that both arbitrary injection is possible and the old behaviour still works.

Docs

I altered the README to highlight that injection works not only for controllers but also for converters and middleware and added two examples.

BC Breaks

None


Any thoughts / objections?

felixfbecker commented 8 years ago

I will squash once you give your OK to merge

jdreesen commented 8 years ago

As I don't have much time right now I just took a quick look. The PR looks good overall, just some small nits. Although I don't really like the code duplications in all those call() methods...

felixfbecker commented 8 years ago

@jdreesen I addressed your comments and squashed.

Although I don't really like the code duplications in all those call() methods...

I know, I don't like it either, but every case (after middleware, before middleware, param converter, controller resolver) is just a bit different so they cannot share the exact same implementation. If you break it down it's really nothing but

I'm actually quite happy that there is no duplication for route / application level middleware and that finish and after middleware can both use AfterMiddlewareCaller.

mnapoli commented 8 years ago

Sorry for the delay, I've taken some time to think about it.

In order to preserve BC we want to inject the request, response and app by:

The application is not a problem because it's a service, but the request & response are not so using the Invoker, even with a custom ResolverChain (to answer your question yes it's possible), will not work because type-hints are only checked for container entries.

What we could do is create a new type of ParameterResolver that checks the type-hint, but not for container entries. Example of usage:

$parameterResolver = new ResolverChain([
    new AssociativeArrayResolver, // inject request/response by name
    new TypeHintResolver, // inject request/response by type-hint
    new TypeHintContainerResolver, // dependency injection
]);

...

$invoker->call($middleware, [
    'request' => $request, // by name, used by AssociativeArrayResolver
    'response' => $response, // by name, used by AssociativeArrayResolver
    'Symfony\...\Request' => $request, // by type-hint, used by TypeHintResolver
    'Symfony\...\Response' => $request, // by type-hint, used by TypeHintResolver
]);

The TypeHintResolver could be reused by other bridges later (e.g. Slim framework). It could be added directly to the Invoker project.

What do you think?

felixfbecker commented 8 years ago

@mnapoli

In order to preserve BC we want to inject the request, response and app by:

  • parameter name

that doesn't make much sense to me - with default Silex there is not injection by parameter name, only by parameter position. A middleware is always called with call_user_func($middleware, $request, $app). I took care of BC by falling back to a numeric parameter resolver. I get injection by parameter name for route parameters, but injecting the request by parameter name is too magic for me. It would also be quite weird to document because for consistency, all parameters including the application should then be able to get injected by name, and the application is always called $app in the docs...

What we could do is create a new type of ParameterResolver that checks the type-hint, but not for container entries

this is a very good solution, but I want to point out here that I strictly followed the existing implementation in ControllerResolver, so this has not much to do with this PR. But it would save a lot of code duplication.

What I don't get though:

$invoker->call($middleware, [
   'request' => $request, // by name, used by AssociativeArrayResolver
   'response' => $response, // by name, used by AssociativeArrayResolver
   'Symfony\...\Request' => $request, // by type-hint, used by TypeHintResolver
   'Symfony\...\Response' => $request, // by type-hint, used by TypeHintResolver
]);

Where do you pass the paramResolver here?

mnapoli commented 8 years ago

The parameter resolvers are passed when constructing the Invoker:

$parameterResolver = new ResolverChain([
    ...
]);
$invoker = new Invoker\Invoker($parameterResolver);

And you are right about injection of request/response by name, that's not necessary here so let's keep it as simple as we can (and it wouldn't be consistent with injection in controllers anyway).

Then you would only need something like this:

$parameterResolver = new ResolverChain([
    new TypeHintResolver, // inject request/response by type-hint
    new TypeHintContainerResolver, // dependency injection
    new NumericArrayResolver, // fallback to injecting "request, response, application"
]);
$invoker = new Invoker\Invoker($parameterResolver);

$invoker->call($middleware, [
    // parameters indexed by position for the NumericArrayResolver
    $request, $response, $application,
    // parameters indexed by the type-hint for the TypeHintResolver
    'Symfony\...\Request' => $request,
    'Symfony\...\Response' => $request,
]);

Does that make sense?

I want to point out here that I strictly followed the existing implementation in ControllerResolver, so this has not much to do with this PR

I agree and take your point. But with this PR this implementation gets duplicated multiple times and the code gets much more complex. By merging this I'll also be committing to supporting this code, I'd rather go with something as simple as possible ;)

felixfbecker commented 8 years ago

@mnapoli Your proposed solution makes a lot of sense. Do you want me to do a PR to Invoker?

mnapoli commented 8 years ago

@felixfbecker if you have time for that that would be awesome!

felixfbecker commented 8 years ago

I changed the implementation to use the TypeHintResolver from my PR https://github.com/PHP-DI/Invoker/pull/8.

When the change is published on Invoker I will bump the dependency version and squash. Just wanted to share the changes now so you can review.

felixfbecker commented 8 years ago

Looks good?

mnapoli commented 8 years ago

Looking very good! Much better now! It's really too bad we have to copy so much code from Silex :/ but that's not your fault of course.

felixfbecker commented 8 years ago

Do you want me to squash so you can merge? Or do you have any other idea for the dispatcher?

mnapoli commented 8 years ago

Thanks for your answers. I'm good for merging, :+1: for squashing

felixfbecker commented 8 years ago

:+1:

felixfbecker commented 8 years ago

It should be good to go, any ETA?

mnapoli commented 8 years ago

A few minutes :)

mnapoli commented 8 years ago

1.5.0 is tagged and released! The documentation was also updated in PHP-DI's docs: http://php-di.org/doc/frameworks/silex.html

Thank you for your patience and implementing all that ;)