diff --git a/composer.json b/composer.json index bc49716..7929cd4 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "require": { "php": ">=5.4", "php-di/php-di": "~5.0", + "php-di/invoker": "~1.2", "silex/silex" : "~1.3", "pimple/pimple" : "~1.1" }, diff --git a/src/Application.php b/src/Application.php index d8afe4d..d501f5b 100644 --- a/src/Application.php +++ b/src/Application.php @@ -7,6 +7,10 @@ use DI\Container; use DI\ContainerBuilder; use Interop\Container\ContainerInterface; +use Invoker\CallableResolver; +use Invoker\ParameterResolver\AssociativeArrayResolver; +use Invoker\ParameterResolver\Container\TypeHintContainerResolver; +use Invoker\ParameterResolver\ResolverChain; /** * Replacement for the Silex Application class to use PHP-DI instead of Pimple. @@ -36,6 +40,8 @@ public function __construct(ContainerBuilder $containerBuilder = null, array $va $containerBuilder = $containerBuilder ?: new ContainerBuilder(); $containerBuilder->addDefinitions([ 'Interop\Container\ContainerInterface' => $this->containerInteropProxy, + 'Silex\Application' => $this, + get_class($this) => $this, ]); $containerBuilder->wrapContainer($this->containerInteropProxy); $this->phpdi = $containerBuilder->build(); @@ -44,7 +50,13 @@ public function __construct(ContainerBuilder $containerBuilder = null, array $va // Override the controller resolver with ours $this['resolver'] = $this->share(function () { - return new ControllerResolver($this->phpdi); + return new ControllerResolver( + new CallableResolver($this->containerInteropProxy), + new ResolverChain([ + new AssociativeArrayResolver, + new TypeHintContainerResolver($this->containerInteropProxy), + ]) + ); }); } diff --git a/src/Controller/ControllerResolver.php b/src/Controller/ControllerResolver.php index 3232db9..0699aa9 100644 --- a/src/Controller/ControllerResolver.php +++ b/src/Controller/ControllerResolver.php @@ -2,7 +2,10 @@ namespace DI\Bridge\Silex\Controller; -use DI\InvokerInterface; +use Invoker\CallableResolver; +use Invoker\Exception\NotCallableException; +use Invoker\ParameterResolver\ParameterResolver; +use Invoker\Reflection\CallableReflection; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; @@ -12,13 +15,25 @@ class ControllerResolver implements ControllerResolverInterface { /** - * @var InvokerInterface + * @var CallableResolver */ - private $invoker; + private $callableResolver; - public function __construct(InvokerInterface $invoker) + /** + * @var ParameterResolver + */ + private $parameterResolver; + + /** + * Constructor. + * + * @param CallableResolver $callableResolver + * @param ParameterResolver $parameterResolver + */ + public function __construct(CallableResolver $callableResolver, ParameterResolver $parameterResolver) { - $this->invoker = $invoker; + $this->callableResolver = $callableResolver; + $this->parameterResolver = $parameterResolver; } /** @@ -26,20 +41,22 @@ public function __construct(InvokerInterface $invoker) */ public function getController(Request $request) { - $controller = $request->attributes->get('_controller'); - - if (! $controller) { - throw new \LogicException('No controller can be found for this request'); + if (! $controller = $request->attributes->get('_controller')) { + throw new \LogicException(sprintf( + 'Controller for URI "%s" could not be found because the "_controller" parameter is missing.', + $request->getPathInfo() + )); } - return function () use ($request, $controller) { - $parameters = [ - 'request' => $request, - ]; - $parameters += $request->attributes->all(); - - return $this->invoker->call($controller, $parameters); - }; + try { + return $this->callableResolver->resolve($controller); + } catch (NotCallableException $e) { + throw new \InvalidArgumentException(sprintf( + 'Controller for URI "%s" is not callable: %s', + $request->getPathInfo(), + $e->getMessage() + )); + } } /** @@ -47,6 +64,38 @@ public function getController(Request $request) */ public function getArguments(Request $request, $controller) { - return array(); + $controllerReflection = CallableReflection::create($controller); + $controllerParameters = $controllerReflection->getParameters(); + $resolvedArguments = []; + + foreach ($controllerParameters as $index => $parameter) { + if ('request' === $parameter->getName() || ($parameter->getClass() && $parameter->getClass()->isInstance($request))) { + $resolvedArguments[$index] = $request; + + break; + } + } + + $arguments = $this->parameterResolver->getParameters( + $controllerReflection, + $request->attributes->all(), + $resolvedArguments + ); + + ksort($arguments); + + // Check if all parameters are resolved + $diff = array_diff_key($controllerParameters, $arguments); + if (0 < count($diff)) { + /** @var \ReflectionParameter $parameter */ + $parameter = reset($diff); + throw new \RuntimeException(sprintf( + 'Controller "%s" requires that you provide a value for the "$%s" argument.', + $controllerReflection->getName(), + $parameter->getName() + )); + } + + return $arguments; } } diff --git a/tests/Fixture/Application.php b/tests/Fixture/Application.php new file mode 100644 index 0000000..43e916c --- /dev/null +++ b/tests/Fixture/Application.php @@ -0,0 +1,7 @@ +createApplication(); - $app->get('/', function (Request $request) { + $app->get('/', function ($request) { return 'Hello ' . $request->get('name'); }); @@ -140,4 +140,52 @@ public function should_pass_the_container_based_on_type_hint() $response = $app->handle(Request::create('/')); $this->assertEquals('bar', $response->getContent()); } + + /** + * @test + */ + public function should_pass_the_silex_application_based_on_type_hint() + { + $app = $this->createApplication(); + $app['foo'] = 'bar'; + + $app->get('/', function (\Silex\Application $a) { + return $a['foo']; + }); + + $response = $app->handle(Request::create('/')); + $this->assertEquals('bar', $response->getContent()); + } + + /** + * @test + */ + public function should_pass_the_own_application_based_on_type_hint() + { + $app = new \DI\Bridge\Silex\Test\Fixture\Application(null, [ + 'foo' => 'bar', + ]); + + $app->get('/', function (\DI\Bridge\Silex\Test\Fixture\Application $a) { + return $a['foo']; + }); + + $response = $app->handle(Request::create('/')); + $this->assertEquals('bar', $response->getContent()); + } + + /** + * @test + */ + public function should_pass_request_object_based_on_type_hint() + { + $app = $this->createApplication(); + + $app->get('/', function (Request $r) { + return 'Hello ' . $r->get('name'); + }); + + $response = $app->handle(Request::create('/?name=john')); + $this->assertEquals('Hello john', $response->getContent()); + } } diff --git a/tests/ProvidersTest.php b/tests/ProvidersTest.php index d536bf9..ea60e52 100644 --- a/tests/ProvidersTest.php +++ b/tests/ProvidersTest.php @@ -2,7 +2,9 @@ namespace DI\Bridge\Silex\Test; +use DI\Bridge\Silex\Test\Fixture\Controller; use DI\ContainerBuilder; +use Silex\Provider\ServiceControllerServiceProvider; use Silex\Provider\SwiftmailerServiceProvider; use Silex\Provider\TwigServiceProvider; use Silex\Provider\UrlGeneratorServiceProvider; @@ -89,4 +91,22 @@ public function test_mailer() $response = $app->handle(Request::create('/')); $this->assertEquals('OK', $response->getContent()); } + + /** + * @see https://github.com/PHP-DI/Silex-Bridge/issues/7 + * @test + */ + public function test_service_controller_service_provider() + { + $app = $this->createApplication(); + + $app->register(new ServiceControllerServiceProvider, [ + 'service.controller' => new Controller, + ]); + + $app->get('/{name}', 'service.controller:hello'); + + $response = $app->handle(Request::create('/john')); + $this->assertEquals('Hello john', $response->getContent()); + } }