From 8e654254f2fc746bfb3fe4b5d635141f24f4e02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 16 Mar 2016 08:48:30 +0100 Subject: [PATCH 1/8] [FrameworkBundle] Introduce autowirable ControllerTrait --- .../FrameworkBundle/Controller/Controller.php | 382 +------------ .../Controller/ControllerTrait.php | 514 ++++++++++++++++++ .../Tests/Controller/ControllerTest.php | 2 +- .../Tests/Controller/ControllerTraitTest.php | 360 ++++++++++++ 4 files changed, 877 insertions(+), 381 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index 90daa3eeadd00..0a130563f3674 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -13,21 +13,6 @@ use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; -use Symfony\Component\HttpFoundation\BinaryFileResponse; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\HttpFoundation\StreamedResponse; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Security\Csrf\CsrfToken; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\Form; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Doctrine\Bundle\DoctrineBundle\Registry; /** * Controller is a simple implementation of a Controller. @@ -35,358 +20,12 @@ * It provides methods to common features needed in controllers. * * @author Fabien Potencier + * @author Kévin Dunglas */ abstract class Controller implements ContainerAwareInterface { use ContainerAwareTrait; - - /** - * Generates a URL from the given parameters. - * - * @param string $route The name of the route - * @param mixed $parameters An array of parameters - * @param int $referenceType The type of reference (one of the constants in UrlGeneratorInterface) - * - * @return string The generated URL - * - * @see UrlGeneratorInterface - */ - protected function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH) - { - return $this->container->get('router')->generate($route, $parameters, $referenceType); - } - - /** - * Forwards the request to another controller. - * - * @param string $controller The controller name (a string like BlogBundle:Post:index) - * @param array $path An array of path parameters - * @param array $query An array of query parameters - * - * @return Response A Response instance - */ - protected function forward($controller, array $path = array(), array $query = array()) - { - $request = $this->container->get('request_stack')->getCurrentRequest(); - $path['_forwarded'] = $request->attributes; - $path['_controller'] = $controller; - $subRequest = $request->duplicate($query, null, $path); - - return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); - } - - /** - * Returns a RedirectResponse to the given URL. - * - * @param string $url The URL to redirect to - * @param int $status The status code to use for the Response - * - * @return RedirectResponse - */ - protected function redirect($url, $status = 302) - { - return new RedirectResponse($url, $status); - } - - /** - * Returns a RedirectResponse to the given route with the given parameters. - * - * @param string $route The name of the route - * @param array $parameters An array of parameters - * @param int $status The status code to use for the Response - * - * @return RedirectResponse - */ - protected function redirectToRoute($route, array $parameters = array(), $status = 302) - { - return $this->redirect($this->generateUrl($route, $parameters), $status); - } - - /** - * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. - * - * @param mixed $data The response data - * @param int $status The status code to use for the Response - * @param array $headers Array of extra headers to add - * @param array $context Context to pass to serializer when using serializer component - * - * @return JsonResponse - */ - protected function json($data, $status = 200, $headers = array(), $context = array()) - { - if ($this->container->has('serializer')) { - $json = $this->container->get('serializer')->serialize($data, 'json', array_merge(array( - 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, - ), $context)); - - return new JsonResponse($json, $status, $headers, true); - } - - return new JsonResponse($data, $status, $headers); - } - - /** - * Returns a BinaryFileResponse object with original or customized file name and disposition header. - * - * @param \SplFileInfo|string $file File object or path to file to be sent as response - * @param string|null $fileName File name to be sent to response or null (will use original file name) - * @param string $disposition Disposition of response ("attachment" is default, other type is "inline") - * - * @return BinaryFileResponse - */ - protected function file($file, $fileName = null, $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT) - { - $response = new BinaryFileResponse($file); - $response->setContentDisposition($disposition, $fileName === null ? $response->getFile()->getFilename() : $fileName); - - return $response; - } - - /** - * Adds a flash message to the current session for type. - * - * @param string $type The type - * @param string $message The message - * - * @throws \LogicException - */ - protected function addFlash($type, $message) - { - if (!$this->container->has('session')) { - throw new \LogicException('You can not use the addFlash method if sessions are disabled.'); - } - - $this->container->get('session')->getFlashBag()->add($type, $message); - } - - /** - * Checks if the attributes are granted against the current authentication token and optionally supplied object. - * - * @param mixed $attributes The attributes - * @param mixed $object The object - * - * @return bool - * - * @throws \LogicException - */ - protected function isGranted($attributes, $object = null) - { - if (!$this->container->has('security.authorization_checker')) { - throw new \LogicException('The SecurityBundle is not registered in your application.'); - } - - return $this->container->get('security.authorization_checker')->isGranted($attributes, $object); - } - - /** - * Throws an exception unless the attributes are granted against the current authentication token and optionally - * supplied object. - * - * @param mixed $attributes The attributes - * @param mixed $object The object - * @param string $message The message passed to the exception - * - * @throws AccessDeniedException - */ - protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.') - { - if (!$this->isGranted($attributes, $object)) { - $exception = $this->createAccessDeniedException($message); - $exception->setAttributes($attributes); - $exception->setSubject($object); - - throw $exception; - } - } - - /** - * Returns a rendered view. - * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * - * @return string The rendered view - */ - protected function renderView($view, array $parameters = array()) - { - if ($this->container->has('templating')) { - return $this->container->get('templating')->render($view, $parameters); - } - - if (!$this->container->has('twig')) { - throw new \LogicException('You can not use the "renderView" method if the Templating Component or the Twig Bundle are not available.'); - } - - return $this->container->get('twig')->render($view, $parameters); - } - - /** - * Renders a view. - * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * @param Response $response A response instance - * - * @return Response A Response instance - */ - protected function render($view, array $parameters = array(), Response $response = null) - { - if ($this->container->has('templating')) { - return $this->container->get('templating')->renderResponse($view, $parameters, $response); - } - - if (!$this->container->has('twig')) { - throw new \LogicException('You can not use the "render" method if the Templating Component or the Twig Bundle are not available.'); - } - - if (null === $response) { - $response = new Response(); - } - - $response->setContent($this->container->get('twig')->render($view, $parameters)); - - return $response; - } - - /** - * Streams a view. - * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * @param StreamedResponse $response A response instance - * - * @return StreamedResponse A StreamedResponse instance - */ - protected function stream($view, array $parameters = array(), StreamedResponse $response = null) - { - if ($this->container->has('templating')) { - $templating = $this->container->get('templating'); - - $callback = function () use ($templating, $view, $parameters) { - $templating->stream($view, $parameters); - }; - } elseif ($this->container->has('twig')) { - $twig = $this->container->get('twig'); - - $callback = function () use ($twig, $view, $parameters) { - $twig->display($view, $parameters); - }; - } else { - throw new \LogicException('You can not use the "stream" method if the Templating Component or the Twig Bundle are not available.'); - } - - if (null === $response) { - return new StreamedResponse($callback); - } - - $response->setCallback($callback); - - return $response; - } - - /** - * Returns a NotFoundHttpException. - * - * This will result in a 404 response code. Usage example: - * - * throw $this->createNotFoundException('Page not found!'); - * - * @param string $message A message - * @param \Exception|null $previous The previous exception - * - * @return NotFoundHttpException - */ - protected function createNotFoundException($message = 'Not Found', \Exception $previous = null) - { - return new NotFoundHttpException($message, $previous); - } - - /** - * Returns an AccessDeniedException. - * - * This will result in a 403 response code. Usage example: - * - * throw $this->createAccessDeniedException('Unable to access this page!'); - * - * @param string $message A message - * @param \Exception|null $previous The previous exception - * - * @return AccessDeniedException - */ - protected function createAccessDeniedException($message = 'Access Denied.', \Exception $previous = null) - { - return new AccessDeniedException($message, $previous); - } - - /** - * Creates and returns a Form instance from the type of the form. - * - * @param string $type The fully qualified class name of the form type - * @param mixed $data The initial data for the form - * @param array $options Options for the form - * - * @return Form - */ - protected function createForm($type, $data = null, array $options = array()) - { - return $this->container->get('form.factory')->create($type, $data, $options); - } - - /** - * Creates and returns a form builder instance. - * - * @param mixed $data The initial data for the form - * @param array $options Options for the form - * - * @return FormBuilder - */ - protected function createFormBuilder($data = null, array $options = array()) - { - return $this->container->get('form.factory')->createBuilder(FormType::class, $data, $options); - } - - /** - * Shortcut to return the Doctrine Registry service. - * - * @return Registry - * - * @throws \LogicException If DoctrineBundle is not available - */ - protected function getDoctrine() - { - if (!$this->container->has('doctrine')) { - throw new \LogicException('The DoctrineBundle is not registered in your application.'); - } - - return $this->container->get('doctrine'); - } - - /** - * Get a user from the Security Token Storage. - * - * @return mixed - * - * @throws \LogicException If SecurityBundle is not available - * - * @see TokenInterface::getUser() - */ - protected function getUser() - { - if (!$this->container->has('security.token_storage')) { - throw new \LogicException('The SecurityBundle is not registered in your application.'); - } - - if (null === $token = $this->container->get('security.token_storage')->getToken()) { - return; - } - - if (!is_object($user = $token->getUser())) { - // e.g. anonymous authentication - return; - } - - return $user; - } + use ControllerTrait; /** * Returns true if the service id is defined. @@ -423,21 +62,4 @@ protected function getParameter($name) { return $this->container->getParameter($name); } - - /** - * Checks the validity of a CSRF token. - * - * @param string $id The id used when generating the token - * @param string $token The actual token sent with the request that should be validated - * - * @return bool - */ - protected function isCsrfTokenValid($id, $token) - { - if (!$this->container->has('security.csrf.token_manager')) { - throw new \LogicException('CSRF protection is not enabled in your application.'); - } - - return $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($id, $token)); - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php new file mode 100644 index 0000000000000..964f7191bd267 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -0,0 +1,514 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Controller; + +use Doctrine\Common\Persistence\ManagerRegistry; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; + +/** + * Common features needed in controllers. + * + * Supports both autowiring trough setters and accessing services using a container. + * + * @author Kévin Dunglas + * @author Fabien Potencier + */ +trait ControllerTrait +{ + /** + * @return RouterInterface + */ + protected function getRouter() + { + if (isset($this->container)) { + return $this->container->get('router'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', RouterInterface::class)); + } + + /** + * @return RequestStack + */ + protected function getRequestStack() + { + if (isset($this->container)) { + return $this->container->get('request_stack'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', RequestStack::class)); + } + + /** + * @return HttpKernelInterface + */ + protected function getHttpKernel() + { + if (isset($this->container)) { + return $this->container->get('http_kernel'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', HttpKernelInterface::class)); + } + + /** + * @return SerializerInterface + */ + protected function getSerializer() + { + if (isset($this->container) && $this->container->has('serializer')) { + return $this->container->get('serializer'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', SerializerInterface::class)); + } + + /** + * Passing the Symfony session implementation is mandatory because flashes are not part of the interface. + * + * @return Session + */ + protected function getSession() + { + if (isset($this->container) && $this->container->has('session')) { + return $this->container->get('session'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', Session::class)); + } + + /** + * @return AuthorizationCheckerInterface + */ + protected function getAuthorizationChecker() + { + if (isset($this->container) && $this->container->has('security.authorization_checker')) { + return $this->container->get('security.authorization_checker'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', AuthorizationCheckerInterface::class)); + } + + /** + * @return EngineInterface + */ + protected function getTemplating() + { + if (isset($this->container) && $this->container->has('templating')) { + return $this->container->get('templating'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', EngineInterface::class)); + } + + /** + * @return \Twig_Environment + */ + protected function getTwig() + { + if (isset($this->container) && $this->container->has('twig')) { + return $this->container->get('twig'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', \Twig_Environment::class)); + } + + /** + * @return ManagerRegistry + */ + protected function getDoctrine() + { + if (isset($this->container) && $this->container->has('doctrine')) { + return $this->container->get('doctrine'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', ManagerRegistry::class)); + } + + /** + * @return FormFactoryInterface + */ + protected function getFormFactory() + { + if (isset($this->container)) { + return $this->container->get('form.factory'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', FormFactoryInterface::class)); + } + + /** + * @return TokenStorageInterface + */ + protected function getTokenStorage() + { + if (isset($this->container) && $this->container->has('security.token_storage')) { + return $this->container->get('security.token_storage'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', TokenStorageInterface::class)); + } + + /** + * @return CsrfTokenManagerInterface + */ + protected function getCsrfTokenManager() + { + if (isset($this->container) && $this->container->has('security.csrf.token_manager')) { + return $this->container->get('security.csrf.token_manager'); + } + + throw new \LogicException(sprintf('An instance of "%s" must be provided.', CsrfTokenManagerInterface::class)); + } + + /** + * Generates a URL from the given parameters. + * + * @param string $route The name of the route + * @param mixed $parameters An array of parameters + * @param int $referenceType The type of reference (one of the constants in UrlGeneratorInterface) + * + * @return string The generated URL + * + * @see UrlGeneratorInterface + */ + protected function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH) + { + return $this->getRouter()->generate($route, $parameters, $referenceType); + } + + /** + * Forwards the request to another controller. + * + * @param string $controller The controller name (a string like BlogBundle:Post:index) + * @param array $path An array of path parameters + * @param array $query An array of query parameters + * + * @return Response A Response instance + */ + protected function forward($controller, array $path = array(), array $query = array()) + { + $request = $this->getRequestStack()->getCurrentRequest(); + $path['_forwarded'] = $request->attributes; + $path['_controller'] = $controller; + $subRequest = $request->duplicate($query, null, $path); + + return $this->getHttpKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + } + + /** + * Returns a RedirectResponse to the given URL. + * + * @param string $url The URL to redirect to + * @param int $status The status code to use for the Response + * + * @return RedirectResponse + */ + protected function redirect($url, $status = 302) + { + return new RedirectResponse($url, $status); + } + + /** + * Returns a RedirectResponse to the given route with the given parameters. + * + * @param string $route The name of the route + * @param array $parameters An array of parameters + * @param int $status The status code to use for the Response + * + * @return RedirectResponse + */ + protected function redirectToRoute($route, array $parameters = array(), $status = 302) + { + return $this->redirect($this->generateUrl($route, $parameters), $status); + } + + /** + * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. + * + * @param mixed $data The response data + * @param int $status The status code to use for the Response + * @param array $headers Array of extra headers to add + * @param array $context Context to pass to serializer when using serializer component + * + * @return JsonResponse + */ + protected function json($data, $status = 200, $headers = array(), $context = array()) + { + try { + $json = $this->getSerializer()->serialize($data, 'json', array_merge(array( + 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, + ), $context)); + + return new JsonResponse($json, $status, $headers, true); + } catch (\LogicException $e) { + return new JsonResponse($data, $status, $headers); + } + } + + /** + * Returns a BinaryFileResponse object with original or customized file name and disposition header. + * + * @param \SplFileInfo|string $file File object or path to file to be sent as response + * @param string|null $fileName File name to be sent to response or null (will use original file name) + * @param string $disposition Disposition of response ("attachment" is default, other type is "inline") + * + * @return BinaryFileResponse + */ + protected function file($file, $fileName = null, $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT) + { + $response = new BinaryFileResponse($file); + $response->setContentDisposition($disposition, $fileName === null ? $response->getFile()->getFilename() : $fileName); + + return $response; + } + + /** + * Adds a flash message to the current session for type. + * + * @param string $type The type + * @param string $message The message + * + * @throws \LogicException + */ + protected function addFlash($type, $message) + { + $this->getSession()->getFlashBag()->add($type, $message); + } + + /** + * Checks if the attributes are granted against the current authentication token and optionally supplied object. + * + * @param mixed $attributes The attributes + * @param mixed $object The object + * + * @return bool + * + * @throws \LogicException + */ + protected function isGranted($attributes, $object = null) + { + return $this->getAuthorizationChecker()->isGranted($attributes, $object); + } + + /** + * Throws an exception unless the attributes are granted against the current authentication token and optionally + * supplied object. + * + * @param mixed $attributes The attributes + * @param mixed $object The object + * @param string $message The message passed to the exception + * + * @throws AccessDeniedException + */ + protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.') + { + if (!$this->isGranted($attributes, $object)) { + $exception = $this->createAccessDeniedException($message); + $exception->setAttributes($attributes); + $exception->setSubject($object); + throw $exception; + } + } + + /** + * Returns a rendered view. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * + * @return string The rendered view + */ + protected function renderView($view, array $parameters = array()) + { + try { + return $this->getTemplating()->render($view, $parameters); + } catch (\LogicException $e) { + return $this->getTwig()->render($view, $parameters); + } + } + + /** + * Renders a view. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * @param Response $response A response instance + * + * @return Response A Response instance + */ + protected function render($view, array $parameters = array(), Response $response = null) + { + try { + return $this->getTemplating()->renderResponse($view, $parameters, $response); + } catch (\LogicException $e) { + if (null === $response) { + $response = new Response(); + } + + return $response->setContent($this->getTwig()->render($view, $parameters)); + } + } + + /** + * Streams a view. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * @param StreamedResponse $response A response instance + * + * @return StreamedResponse A StreamedResponse instance + */ + protected function stream($view, array $parameters = array(), StreamedResponse $response = null) + { + try { + $templating = $this->getTemplating(); + + $callback = function () use ($templating, $view, $parameters) { + $templating->stream($view, $parameters); + }; + } catch (\LogicException $e) { + $twig = $this->getTwig(); + + $callback = function () use ($twig, $view, $parameters) { + $twig->display($view, $parameters); + }; + } + + if (null === $response) { + return new StreamedResponse($callback); + } + + $response->setCallback($callback); + + return $response; + } + + /** + * Returns a NotFoundHttpException. + * + * This will result in a 404 response code. Usage example: + * + * throw $this->createNotFoundException('Page not found!'); + * + * @param string $message A message + * @param \Exception|null $previous The previous exception + * + * @return NotFoundHttpException + */ + protected function createNotFoundException($message = 'Not Found', \Exception $previous = null) + { + return new NotFoundHttpException($message, $previous); + } + + /** + * Returns an AccessDeniedException. + * + * This will result in a 403 response code. Usage example: + * + * throw $this->createAccessDeniedException('Unable to access this page!'); + * + * @param string $message A message + * @param \Exception|null $previous The previous exception + * + * @return AccessDeniedException + */ + protected function createAccessDeniedException($message = 'Access Denied.', \Exception $previous = null) + { + return new AccessDeniedException($message, $previous); + } + + /** + * Creates and returns a Form instance from the type of the form. + * + * @param string $type The fully qualified class name of the form type + * @param mixed $data The initial data for the form + * @param array $options Options for the form + * + * @return Form + */ + protected function createForm($type, $data = null, array $options = array()) + { + return $this->getFormFactory()->create($type, $data, $options); + } + + /** + * Creates and returns a form builder instance. + * + * @param mixed $data The initial data for the form + * @param array $options Options for the form + * + * @return FormBuilder + */ + protected function createFormBuilder($data = null, array $options = array()) + { + return $this->getFormFactory()->createBuilder(FormType::class, $data, $options); + } + + /** + * Get a user from the Security Token Storage. + * + * @return mixed + * + * @throws \LogicException If SecurityBundle is not available + * + * @see TokenInterface::getUser() + */ + protected function getUser() + { + if (null === $token = $this->getTokenStorage()->getToken()) { + return; + } + + if (!is_object($user = $token->getUser())) { + // e.g. anonymous authentication + return; + } + + return $user; + } + + /** + * Checks the validity of a CSRF token. + * + * @param string $id The id used when generating the token + * @param string $token The actual token sent with the request that should be validated + * + * @return bool + */ + protected function isCsrfTokenValid($id, $token) + { + return $this->getCsrfTokenManager()->isTokenValid(new CsrfToken($id, $token)); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php index 80908a9e5b91f..5ef97158bc33d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php @@ -87,7 +87,7 @@ public function testGetUserWithEmptyTokenStorage() /** * @expectedException \LogicException - * @expectedExceptionMessage The SecurityBundle is not registered in your application. + * @expectedExceptionMessage An instance of "Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface" must be provided. */ public function testGetUserWithEmptyContainer() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php new file mode 100644 index 0000000000000..4abb99dc425df --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -0,0 +1,360 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; + +use Doctrine\Common\Persistence\ManagerRegistry; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; + +/** + * @author Kévin Dunglas + */ +class ControllerTraitTest extends \PHPUnit_Framework_TestCase +{ + use ControllerTrait { + getSerializer as traitGetSerializer; + getTemplating as traitGetTemplating; + } + + private $token; + private $serializer; + private $flashBag; + private $isGranted; + private $templating; + private $twig; + private $formFactory; + + protected function getRouter() + { + $router = $this->getMock(RouterInterface::class); + $router->expects($this->once())->method('generate')->willReturn('/foo'); + + return $router; + } + + protected function getRequestStack() + { + $request = Request::create('/'); + $request->setLocale('fr'); + $request->setRequestFormat('xml'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + + return $requestStack; + } + + protected function getHttpKernel() + { + $kernel = $this->getMock(HttpKernelInterface::class); + $kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { + return new Response($request->getRequestFormat().'--'.$request->getLocale()); + })); + + return $kernel; + } + + protected function getSerializer() + { + if ($this->serializer) { + return $this->serializer; + } + + return $this->traitGetSerializer(); + } + + protected function getSession() + { + $session = $this->getMock(Session::class); + $session->expects($this->once())->method('getFlashBag')->willReturn($this->flashBag); + + return $session; + } + + protected function getAuthorizationChecker() + { + $authorizationChecker = $this->getMock(AuthorizationCheckerInterface::class); + $authorizationChecker->expects($this->once())->method('isGranted')->willReturn($this->isGranted); + + return $authorizationChecker; + } + + protected function getTemplating() + { + if ($this->templating) { + return $this->templating; + } + + return $this->traitGetTemplating(); + } + + protected function getTwig() + { + return $this->twig; + } + + protected function getDoctrine() + { + return $this->getMock(ManagerRegistry::class); + } + + protected function getFormFactory() + { + return $this->formFactory; + } + + protected function getTokenStorage() + { + $tokenStorage = $this->getMock(TokenStorageInterface::class); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($this->token)); + + return $tokenStorage; + } + + protected function getCsrfTokenManager() + { + $tokenManager = $this->getMock(CsrfTokenManagerInterface::class); + $tokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); + + return $tokenManager; + } + + public function testGenerateUrl() + { + $this->assertEquals('/foo', $this->generateUrl('foo')); + } + + public function testForward() + { + $response = $this->forward('a_controller'); + $this->assertEquals('xml--fr', $response->getContent()); + } + + public function testRedirect() + { + $response = $this->redirect('http://example.com', 301); + + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertSame('http://example.com', $response->getTargetUrl()); + $this->assertSame(301, $response->getStatusCode()); + } + + public function testRedirectToRoute() + { + $response = $this->redirectToRoute('foo'); + + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertSame('/foo', $response->getTargetUrl()); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testGetUser() + { + $user = new User('user', 'pass'); + $this->token = new UsernamePasswordToken($user, 'pass', 'default', array('ROLE_USER')); + + $this->assertSame($this->getUser(), $user); + } + + public function testGetUserAnonymousUserConvertedToNull() + { + $this->token = new AnonymousToken('default', 'anon.'); + $this->assertNull($this->getUser()); + } + + public function testGetUserWithEmptyTokenStorage() + { + $this->token = null; + $this->assertNull($this->getUser()); + } + + public function testJson() + { + $this->serializer = null; + + $response = $this->json(array()); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('[]', $response->getContent()); + } + + public function testJsonWithSerializer() + { + $this->serializer = $this->getMock(SerializerInterface::class); + $this->serializer + ->expects($this->once()) + ->method('serialize') + ->with(array(), 'json', array('json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS)) + ->will($this->returnValue('[]')); + + $response = $this->json(array()); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('[]', $response->getContent()); + } + + public function testJsonWithSerializerContextOverride() + { + $this->serializer = $this->getMock(SerializerInterface::class); + $this->serializer + ->expects($this->once()) + ->method('serialize') + ->with(array(), 'json', array('json_encode_options' => 0, 'other' => 'context')) + ->will($this->returnValue('[]')); + + $response = $this->json(array(), 200, array(), array('json_encode_options' => 0, 'other' => 'context')); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('[]', $response->getContent()); + $response->setEncodingOptions(JSON_FORCE_OBJECT); + $this->assertEquals('{}', $response->getContent()); + } + + public function testAddFlash() + { + $this->flashBag = new FlashBag(); + + $this->addFlash('foo', 'bar'); + + $this->assertSame(array('bar'), $this->flashBag->get('foo')); + } + + public function testIsGranted() + { + $this->isGranted = true; + + $this->assertTrue($this->isGranted('foo')); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException + */ + public function testDenyAccessUnlessGranted() + { + $this->isGranted = false; + + $this->denyAccessUnlessGranted('foo'); + } + + public function testRenderViewTemplating() + { + $this->templating = $this->getMock(EngineInterface::class); + $this->templating->expects($this->once())->method('render')->willReturn('bar'); + + $this->assertEquals('bar', $this->renderView('foo')); + } + + public function testRenderViewTwig() + { + $this->templating = false; + $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); + $this->twig->expects($this->once())->method('render')->willReturn('bar'); + + $this->assertEquals('bar', $this->renderView('foo')); + } + + public function testRenderTemplating() + { + $this->templating = $this->getMock(EngineInterface::class); + $this->templating->expects($this->once())->method('renderResponse')->willReturn(new Response('bar')); + + $this->assertEquals('bar', $this->render('foo')->getContent()); + } + + public function testRenderTwig() + { + $this->templating = false; + $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); + $this->twig->expects($this->once())->method('render')->willReturn('bar'); + + $this->assertEquals('bar', $this->render('foo')->getContent()); + } + + public function testStreamTemplating() + { + $this->templating = $this->getMock(EngineInterface::class); + + $this->assertInstanceOf(StreamedResponse::class, $this->stream('foo')); + } + + public function testStreamTwig() + { + $this->templating = false; + $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); + + $this->assertInstanceOf(StreamedResponse::class, $this->stream('foo')); + } + + public function testCreateNotFoundException() + { + $this->assertInstanceOf(NotFoundHttpException::class, $this->createNotFoundException()); + } + + public function testCreateAccessDeniedException() + { + $this->assertInstanceOf(AccessDeniedException::class, $this->createAccessDeniedException()); + } + + public function testCreateForm() + { + $form = $this->getMock(FormInterface::class); + + $this->formFactory = $this->getMock(FormFactoryInterface::class); + $this->formFactory->expects($this->once())->method('create')->willReturn($form); + + $this->assertEquals($form, $this->createForm('foo')); + } + + public function testCreateFormBuilder() + { + $formBuilder = $this->getMock(FormBuilderInterface::class); + + $this->formFactory = $this->getMock(FormFactoryInterface::class); + $this->formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder); + + $this->assertEquals($formBuilder, $this->createFormBuilder('foo')); + } + + public function testGetDoctrine() + { + $this->assertInstanceOf(ManagerRegistry::class, $this->getDoctrine()); + } + + public function testIsCsrfTokenValid() + { + $this->assertTrue($this->isCsrfTokenValid('foo', 'bar')); + } +} From ef6d676589b7fbbeafbd3b5f42fbe15b0a5b7d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 11 Jan 2017 08:20:30 +0100 Subject: [PATCH 2/8] Make ControllerTrait compatible with getter injection --- .../FrameworkBundle/Controller/Controller.php | 382 +++++++++++++++++- .../Controller/ControllerTrait.php | 153 ++----- .../Tests/Controller/ControllerTest.php | 2 +- .../Tests/Controller/ControllerTraitTest.php | 2 - 4 files changed, 417 insertions(+), 122 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index 0a130563f3674..90daa3eeadd00 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -13,6 +13,21 @@ use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Doctrine\Bundle\DoctrineBundle\Registry; /** * Controller is a simple implementation of a Controller. @@ -20,12 +35,358 @@ * It provides methods to common features needed in controllers. * * @author Fabien Potencier - * @author Kévin Dunglas */ abstract class Controller implements ContainerAwareInterface { use ContainerAwareTrait; - use ControllerTrait; + + /** + * Generates a URL from the given parameters. + * + * @param string $route The name of the route + * @param mixed $parameters An array of parameters + * @param int $referenceType The type of reference (one of the constants in UrlGeneratorInterface) + * + * @return string The generated URL + * + * @see UrlGeneratorInterface + */ + protected function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH) + { + return $this->container->get('router')->generate($route, $parameters, $referenceType); + } + + /** + * Forwards the request to another controller. + * + * @param string $controller The controller name (a string like BlogBundle:Post:index) + * @param array $path An array of path parameters + * @param array $query An array of query parameters + * + * @return Response A Response instance + */ + protected function forward($controller, array $path = array(), array $query = array()) + { + $request = $this->container->get('request_stack')->getCurrentRequest(); + $path['_forwarded'] = $request->attributes; + $path['_controller'] = $controller; + $subRequest = $request->duplicate($query, null, $path); + + return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + } + + /** + * Returns a RedirectResponse to the given URL. + * + * @param string $url The URL to redirect to + * @param int $status The status code to use for the Response + * + * @return RedirectResponse + */ + protected function redirect($url, $status = 302) + { + return new RedirectResponse($url, $status); + } + + /** + * Returns a RedirectResponse to the given route with the given parameters. + * + * @param string $route The name of the route + * @param array $parameters An array of parameters + * @param int $status The status code to use for the Response + * + * @return RedirectResponse + */ + protected function redirectToRoute($route, array $parameters = array(), $status = 302) + { + return $this->redirect($this->generateUrl($route, $parameters), $status); + } + + /** + * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. + * + * @param mixed $data The response data + * @param int $status The status code to use for the Response + * @param array $headers Array of extra headers to add + * @param array $context Context to pass to serializer when using serializer component + * + * @return JsonResponse + */ + protected function json($data, $status = 200, $headers = array(), $context = array()) + { + if ($this->container->has('serializer')) { + $json = $this->container->get('serializer')->serialize($data, 'json', array_merge(array( + 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, + ), $context)); + + return new JsonResponse($json, $status, $headers, true); + } + + return new JsonResponse($data, $status, $headers); + } + + /** + * Returns a BinaryFileResponse object with original or customized file name and disposition header. + * + * @param \SplFileInfo|string $file File object or path to file to be sent as response + * @param string|null $fileName File name to be sent to response or null (will use original file name) + * @param string $disposition Disposition of response ("attachment" is default, other type is "inline") + * + * @return BinaryFileResponse + */ + protected function file($file, $fileName = null, $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT) + { + $response = new BinaryFileResponse($file); + $response->setContentDisposition($disposition, $fileName === null ? $response->getFile()->getFilename() : $fileName); + + return $response; + } + + /** + * Adds a flash message to the current session for type. + * + * @param string $type The type + * @param string $message The message + * + * @throws \LogicException + */ + protected function addFlash($type, $message) + { + if (!$this->container->has('session')) { + throw new \LogicException('You can not use the addFlash method if sessions are disabled.'); + } + + $this->container->get('session')->getFlashBag()->add($type, $message); + } + + /** + * Checks if the attributes are granted against the current authentication token and optionally supplied object. + * + * @param mixed $attributes The attributes + * @param mixed $object The object + * + * @return bool + * + * @throws \LogicException + */ + protected function isGranted($attributes, $object = null) + { + if (!$this->container->has('security.authorization_checker')) { + throw new \LogicException('The SecurityBundle is not registered in your application.'); + } + + return $this->container->get('security.authorization_checker')->isGranted($attributes, $object); + } + + /** + * Throws an exception unless the attributes are granted against the current authentication token and optionally + * supplied object. + * + * @param mixed $attributes The attributes + * @param mixed $object The object + * @param string $message The message passed to the exception + * + * @throws AccessDeniedException + */ + protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.') + { + if (!$this->isGranted($attributes, $object)) { + $exception = $this->createAccessDeniedException($message); + $exception->setAttributes($attributes); + $exception->setSubject($object); + + throw $exception; + } + } + + /** + * Returns a rendered view. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * + * @return string The rendered view + */ + protected function renderView($view, array $parameters = array()) + { + if ($this->container->has('templating')) { + return $this->container->get('templating')->render($view, $parameters); + } + + if (!$this->container->has('twig')) { + throw new \LogicException('You can not use the "renderView" method if the Templating Component or the Twig Bundle are not available.'); + } + + return $this->container->get('twig')->render($view, $parameters); + } + + /** + * Renders a view. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * @param Response $response A response instance + * + * @return Response A Response instance + */ + protected function render($view, array $parameters = array(), Response $response = null) + { + if ($this->container->has('templating')) { + return $this->container->get('templating')->renderResponse($view, $parameters, $response); + } + + if (!$this->container->has('twig')) { + throw new \LogicException('You can not use the "render" method if the Templating Component or the Twig Bundle are not available.'); + } + + if (null === $response) { + $response = new Response(); + } + + $response->setContent($this->container->get('twig')->render($view, $parameters)); + + return $response; + } + + /** + * Streams a view. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * @param StreamedResponse $response A response instance + * + * @return StreamedResponse A StreamedResponse instance + */ + protected function stream($view, array $parameters = array(), StreamedResponse $response = null) + { + if ($this->container->has('templating')) { + $templating = $this->container->get('templating'); + + $callback = function () use ($templating, $view, $parameters) { + $templating->stream($view, $parameters); + }; + } elseif ($this->container->has('twig')) { + $twig = $this->container->get('twig'); + + $callback = function () use ($twig, $view, $parameters) { + $twig->display($view, $parameters); + }; + } else { + throw new \LogicException('You can not use the "stream" method if the Templating Component or the Twig Bundle are not available.'); + } + + if (null === $response) { + return new StreamedResponse($callback); + } + + $response->setCallback($callback); + + return $response; + } + + /** + * Returns a NotFoundHttpException. + * + * This will result in a 404 response code. Usage example: + * + * throw $this->createNotFoundException('Page not found!'); + * + * @param string $message A message + * @param \Exception|null $previous The previous exception + * + * @return NotFoundHttpException + */ + protected function createNotFoundException($message = 'Not Found', \Exception $previous = null) + { + return new NotFoundHttpException($message, $previous); + } + + /** + * Returns an AccessDeniedException. + * + * This will result in a 403 response code. Usage example: + * + * throw $this->createAccessDeniedException('Unable to access this page!'); + * + * @param string $message A message + * @param \Exception|null $previous The previous exception + * + * @return AccessDeniedException + */ + protected function createAccessDeniedException($message = 'Access Denied.', \Exception $previous = null) + { + return new AccessDeniedException($message, $previous); + } + + /** + * Creates and returns a Form instance from the type of the form. + * + * @param string $type The fully qualified class name of the form type + * @param mixed $data The initial data for the form + * @param array $options Options for the form + * + * @return Form + */ + protected function createForm($type, $data = null, array $options = array()) + { + return $this->container->get('form.factory')->create($type, $data, $options); + } + + /** + * Creates and returns a form builder instance. + * + * @param mixed $data The initial data for the form + * @param array $options Options for the form + * + * @return FormBuilder + */ + protected function createFormBuilder($data = null, array $options = array()) + { + return $this->container->get('form.factory')->createBuilder(FormType::class, $data, $options); + } + + /** + * Shortcut to return the Doctrine Registry service. + * + * @return Registry + * + * @throws \LogicException If DoctrineBundle is not available + */ + protected function getDoctrine() + { + if (!$this->container->has('doctrine')) { + throw new \LogicException('The DoctrineBundle is not registered in your application.'); + } + + return $this->container->get('doctrine'); + } + + /** + * Get a user from the Security Token Storage. + * + * @return mixed + * + * @throws \LogicException If SecurityBundle is not available + * + * @see TokenInterface::getUser() + */ + protected function getUser() + { + if (!$this->container->has('security.token_storage')) { + throw new \LogicException('The SecurityBundle is not registered in your application.'); + } + + if (null === $token = $this->container->get('security.token_storage')->getToken()) { + return; + } + + if (!is_object($user = $token->getUser())) { + // e.g. anonymous authentication + return; + } + + return $user; + } /** * Returns true if the service id is defined. @@ -62,4 +423,21 @@ protected function getParameter($name) { return $this->container->getParameter($name); } + + /** + * Checks the validity of a CSRF token. + * + * @param string $id The id used when generating the token + * @param string $token The actual token sent with the request that should be validated + * + * @return bool + */ + protected function isCsrfTokenValid($id, $token) + { + if (!$this->container->has('security.csrf.token_manager')) { + throw new \LogicException('CSRF protection is not enabled in your application.'); + } + + return $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($id, $token)); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 964f7191bd267..ff920b2d0a9a0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -12,7 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; use Doctrine\Common\Persistence\ManagerRegistry; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RequestStack; @@ -29,8 +31,6 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\Form; -use Symfony\Component\Form\FormBuilder; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -42,153 +42,72 @@ * Supports both autowiring trough setters and accessing services using a container. * * @author Kévin Dunglas - * @author Fabien Potencier */ trait ControllerTrait { - /** - * @return RouterInterface - */ - protected function getRouter() + protected function getRouter(): RouterInterface { - if (isset($this->container)) { - return $this->container->get('router'); - } - - throw new \LogicException(sprintf('An instance of "%s" must be provided.', RouterInterface::class)); + throw new \LogicException(sprintf('An instance of "%" must be provided.', RouterInterface::class)); } - /** - * @return RequestStack - */ - protected function getRequestStack() + protected function getRequestStack(): RequestStack { - if (isset($this->container)) { - return $this->container->get('request_stack'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', RequestStack::class)); } - /** - * @return HttpKernelInterface - */ - protected function getHttpKernel() + protected function getHttpKernel(): HttpKernelInterface { - if (isset($this->container)) { - return $this->container->get('http_kernel'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', HttpKernelInterface::class)); } - /** - * @return SerializerInterface - */ - protected function getSerializer() + protected function getSerializer(): SerializerInterface { - if (isset($this->container) && $this->container->has('serializer')) { - return $this->container->get('serializer'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', SerializerInterface::class)); } /** - * Passing the Symfony session implementation is mandatory because flashes are not part of the interface. + * An instance of the Session implementation (and not the interface) is returned because getFlashBag is not part of + * the interface. * * @return Session */ - protected function getSession() + protected function getSession(): Session { - if (isset($this->container) && $this->container->has('session')) { - return $this->container->get('session'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', Session::class)); } - /** - * @return AuthorizationCheckerInterface - */ - protected function getAuthorizationChecker() + protected function getAuthorizationChecker(): AuthorizationCheckerInterface { - if (isset($this->container) && $this->container->has('security.authorization_checker')) { - return $this->container->get('security.authorization_checker'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', AuthorizationCheckerInterface::class)); } - /** - * @return EngineInterface - */ - protected function getTemplating() + protected function getTemplating(): EngineInterface { - if (isset($this->container) && $this->container->has('templating')) { - return $this->container->get('templating'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', EngineInterface::class)); } - /** - * @return \Twig_Environment - */ - protected function getTwig() + protected function getTwig(): \Twig_Environment { - if (isset($this->container) && $this->container->has('twig')) { - return $this->container->get('twig'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', \Twig_Environment::class)); } - /** - * @return ManagerRegistry - */ - protected function getDoctrine() + protected function getDoctrine(): ManagerRegistry { - if (isset($this->container) && $this->container->has('doctrine')) { - return $this->container->get('doctrine'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', ManagerRegistry::class)); } - /** - * @return FormFactoryInterface - */ - protected function getFormFactory() + protected function getFormFactory(): FormFactoryInterface { - if (isset($this->container)) { - return $this->container->get('form.factory'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', FormFactoryInterface::class)); } - /** - * @return TokenStorageInterface - */ - protected function getTokenStorage() + protected function getTokenStorage(): TokenStorageInterface { - if (isset($this->container) && $this->container->has('security.token_storage')) { - return $this->container->get('security.token_storage'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', TokenStorageInterface::class)); } - /** - * @return CsrfTokenManagerInterface - */ - protected function getCsrfTokenManager() + protected function getCsrfTokenManager(): CsrfTokenManagerInterface { - if (isset($this->container) && $this->container->has('security.csrf.token_manager')) { - return $this->container->get('security.csrf.token_manager'); - } - throw new \LogicException(sprintf('An instance of "%s" must be provided.', CsrfTokenManagerInterface::class)); } @@ -203,7 +122,7 @@ protected function getCsrfTokenManager() * * @see UrlGeneratorInterface */ - protected function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH) + protected function generateUrl(string $route, array $parameters = array(), int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string { return $this->getRouter()->generate($route, $parameters, $referenceType); } @@ -217,7 +136,7 @@ protected function generateUrl($route, $parameters = array(), $referenceType = U * * @return Response A Response instance */ - protected function forward($controller, array $path = array(), array $query = array()) + protected function forward(string $controller, array $path = array(), array $query = array()): Response { $request = $this->getRequestStack()->getCurrentRequest(); $path['_forwarded'] = $request->attributes; @@ -235,7 +154,7 @@ protected function forward($controller, array $path = array(), array $query = ar * * @return RedirectResponse */ - protected function redirect($url, $status = 302) + protected function redirect(string $url, int $status = 302): RedirectResponse { return new RedirectResponse($url, $status); } @@ -249,7 +168,7 @@ protected function redirect($url, $status = 302) * * @return RedirectResponse */ - protected function redirectToRoute($route, array $parameters = array(), $status = 302) + protected function redirectToRoute(string $route, array $parameters = array(), int $status = 302): RedirectResponse { return $this->redirect($this->generateUrl($route, $parameters), $status); } @@ -264,7 +183,7 @@ protected function redirectToRoute($route, array $parameters = array(), $status * * @return JsonResponse */ - protected function json($data, $status = 200, $headers = array(), $context = array()) + protected function json($data, int $status = 200, array $headers = array(), array $context = array()): JsonResponse { try { $json = $this->getSerializer()->serialize($data, 'json', array_merge(array( @@ -286,7 +205,7 @@ protected function json($data, $status = 200, $headers = array(), $context = arr * * @return BinaryFileResponse */ - protected function file($file, $fileName = null, $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT) + protected function file($file, string $fileName = null, string $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT): BinaryFileResponse { $response = new BinaryFileResponse($file); $response->setContentDisposition($disposition, $fileName === null ? $response->getFile()->getFilename() : $fileName); @@ -302,7 +221,7 @@ protected function file($file, $fileName = null, $disposition = ResponseHeaderBa * * @throws \LogicException */ - protected function addFlash($type, $message) + protected function addFlash(string $type, string $message) { $this->getSession()->getFlashBag()->add($type, $message); } @@ -317,7 +236,7 @@ protected function addFlash($type, $message) * * @throws \LogicException */ - protected function isGranted($attributes, $object = null) + protected function isGranted($attributes, $object = null): bool { return $this->getAuthorizationChecker()->isGranted($attributes, $object); } @@ -332,7 +251,7 @@ protected function isGranted($attributes, $object = null) * * @throws AccessDeniedException */ - protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.') + protected function denyAccessUnlessGranted($attributes, $object = null, string $message = 'Access Denied.') { if (!$this->isGranted($attributes, $object)) { $exception = $this->createAccessDeniedException($message); @@ -350,7 +269,7 @@ protected function denyAccessUnlessGranted($attributes, $object = null, $message * * @return string The rendered view */ - protected function renderView($view, array $parameters = array()) + protected function renderView(string $view, array $parameters = array()): string { try { return $this->getTemplating()->render($view, $parameters); @@ -368,7 +287,7 @@ protected function renderView($view, array $parameters = array()) * * @return Response A Response instance */ - protected function render($view, array $parameters = array(), Response $response = null) + protected function render(string $view, array $parameters = array(), Response $response = null): Response { try { return $this->getTemplating()->renderResponse($view, $parameters, $response); @@ -390,7 +309,7 @@ protected function render($view, array $parameters = array(), Response $response * * @return StreamedResponse A StreamedResponse instance */ - protected function stream($view, array $parameters = array(), StreamedResponse $response = null) + protected function stream(string $view, array $parameters = array(), StreamedResponse $response = null): StreamedResponse { try { $templating = $this->getTemplating(); @@ -427,7 +346,7 @@ protected function stream($view, array $parameters = array(), StreamedResponse $ * * @return NotFoundHttpException */ - protected function createNotFoundException($message = 'Not Found', \Exception $previous = null) + protected function createNotFoundException(string $message = 'Not Found', \Exception $previous = null): NotFoundHttpException { return new NotFoundHttpException($message, $previous); } @@ -444,7 +363,7 @@ protected function createNotFoundException($message = 'Not Found', \Exception $p * * @return AccessDeniedException */ - protected function createAccessDeniedException($message = 'Access Denied.', \Exception $previous = null) + protected function createAccessDeniedException(string $message = 'Access Denied.', \Exception $previous = null): AccessDeniedException { return new AccessDeniedException($message, $previous); } @@ -456,9 +375,9 @@ protected function createAccessDeniedException($message = 'Access Denied.', \Exc * @param mixed $data The initial data for the form * @param array $options Options for the form * - * @return Form + * @return FormInterface */ - protected function createForm($type, $data = null, array $options = array()) + protected function createForm(string $type, $data = null, array $options = array()): FormInterface { return $this->getFormFactory()->create($type, $data, $options); } @@ -469,9 +388,9 @@ protected function createForm($type, $data = null, array $options = array()) * @param mixed $data The initial data for the form * @param array $options Options for the form * - * @return FormBuilder + * @return FormBuilderInterface */ - protected function createFormBuilder($data = null, array $options = array()) + protected function createFormBuilder($data = null, array $options = array()): FormBuilderInterface { return $this->getFormFactory()->createBuilder(FormType::class, $data, $options); } @@ -507,7 +426,7 @@ protected function getUser() * * @return bool */ - protected function isCsrfTokenValid($id, $token) + protected function isCsrfTokenValid(string $id, string $token): bool { return $this->getCsrfTokenManager()->isTokenValid(new CsrfToken($id, $token)); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php index 5ef97158bc33d..80908a9e5b91f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php @@ -87,7 +87,7 @@ public function testGetUserWithEmptyTokenStorage() /** * @expectedException \LogicException - * @expectedExceptionMessage An instance of "Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface" must be provided. + * @expectedExceptionMessage The SecurityBundle is not registered in your application. */ public function testGetUserWithEmptyContainer() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index 4abb99dc425df..5748c4c60cf92 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -29,7 +29,6 @@ use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; @@ -73,7 +72,6 @@ protected function getRequestStack() $requestStack = new RequestStack(); $requestStack->push($request); - return $requestStack; } From ef99840e998ee6df04444989f60b7c8c85112813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 13 Feb 2017 10:21:52 +0100 Subject: [PATCH 3/8] Mark experimental and remove Templating support --- .../Controller/ControllerTrait.php | 44 ++++-------- .../Tests/Controller/ControllerTraitTest.php | 67 +++++-------------- 2 files changed, 27 insertions(+), 84 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index ff920b2d0a9a0..316238fa5fefb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -34,14 +34,15 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; /** * Common features needed in controllers. * - * Supports both autowiring trough setters and accessing services using a container. + * The recommended way of injecting dependencies is trough getter injection. * * @author Kévin Dunglas + * + * @experimental in version 3.3 */ trait ControllerTrait { @@ -81,11 +82,6 @@ protected function getAuthorizationChecker(): AuthorizationCheckerInterface throw new \LogicException(sprintf('An instance of "%s" must be provided.', AuthorizationCheckerInterface::class)); } - protected function getTemplating(): EngineInterface - { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', EngineInterface::class)); - } - protected function getTwig(): \Twig_Environment { throw new \LogicException(sprintf('An instance of "%s" must be provided.', \Twig_Environment::class)); @@ -271,11 +267,7 @@ protected function denyAccessUnlessGranted($attributes, $object = null, string $ */ protected function renderView(string $view, array $parameters = array()): string { - try { - return $this->getTemplating()->render($view, $parameters); - } catch (\LogicException $e) { - return $this->getTwig()->render($view, $parameters); - } + return $this->getTwig()->render($view, $parameters); } /** @@ -289,15 +281,11 @@ protected function renderView(string $view, array $parameters = array()): string */ protected function render(string $view, array $parameters = array(), Response $response = null): Response { - try { - return $this->getTemplating()->renderResponse($view, $parameters, $response); - } catch (\LogicException $e) { - if (null === $response) { - $response = new Response(); - } - - return $response->setContent($this->getTwig()->render($view, $parameters)); + if (null === $response) { + $response = new Response(); } + + return $response->setContent($this->getTwig()->render($view, $parameters)); } /** @@ -311,19 +299,11 @@ protected function render(string $view, array $parameters = array(), Response $r */ protected function stream(string $view, array $parameters = array(), StreamedResponse $response = null): StreamedResponse { - try { - $templating = $this->getTemplating(); - - $callback = function () use ($templating, $view, $parameters) { - $templating->stream($view, $parameters); - }; - } catch (\LogicException $e) { - $twig = $this->getTwig(); + $twig = $this->getTwig(); - $callback = function () use ($twig, $view, $parameters) { - $twig->display($view, $parameters); - }; - } + $callback = function () use ($twig, $view, $parameters) { + $twig->display($view, $parameters); + }; if (null === $response) { return new StreamedResponse($callback); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index 5748c4c60cf92..acd05ba283553 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -35,29 +35,28 @@ use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; /** * @author Kévin Dunglas + * + * @requires PHP 7 */ class ControllerTraitTest extends \PHPUnit_Framework_TestCase { use ControllerTrait { getSerializer as traitGetSerializer; - getTemplating as traitGetTemplating; } private $token; private $serializer; private $flashBag; private $isGranted; - private $templating; private $twig; private $formFactory; protected function getRouter() { - $router = $this->getMock(RouterInterface::class); + $router = $this->getMockBuilder(RouterInterface::class)->getMock(); $router->expects($this->once())->method('generate')->willReturn('/foo'); return $router; @@ -77,7 +76,7 @@ protected function getRequestStack() protected function getHttpKernel() { - $kernel = $this->getMock(HttpKernelInterface::class); + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); $kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { return new Response($request->getRequestFormat().'--'.$request->getLocale()); })); @@ -96,7 +95,7 @@ protected function getSerializer() protected function getSession() { - $session = $this->getMock(Session::class); + $session = $this->getMockBuilder(Session::class)->getMock(); $session->expects($this->once())->method('getFlashBag')->willReturn($this->flashBag); return $session; @@ -104,21 +103,12 @@ protected function getSession() protected function getAuthorizationChecker() { - $authorizationChecker = $this->getMock(AuthorizationCheckerInterface::class); + $authorizationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock(); $authorizationChecker->expects($this->once())->method('isGranted')->willReturn($this->isGranted); return $authorizationChecker; } - protected function getTemplating() - { - if ($this->templating) { - return $this->templating; - } - - return $this->traitGetTemplating(); - } - protected function getTwig() { return $this->twig; @@ -126,7 +116,7 @@ protected function getTwig() protected function getDoctrine() { - return $this->getMock(ManagerRegistry::class); + return $this->getMockBuilder(ManagerRegistry::class)->getMock(); } protected function getFormFactory() @@ -136,7 +126,7 @@ protected function getFormFactory() protected function getTokenStorage() { - $tokenStorage = $this->getMock(TokenStorageInterface::class); + $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); $tokenStorage ->expects($this->once()) ->method('getToken') @@ -147,7 +137,7 @@ protected function getTokenStorage() protected function getCsrfTokenManager() { - $tokenManager = $this->getMock(CsrfTokenManagerInterface::class); + $tokenManager = $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock(); $tokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); return $tokenManager; @@ -213,7 +203,7 @@ public function testJson() public function testJsonWithSerializer() { - $this->serializer = $this->getMock(SerializerInterface::class); + $this->serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); $this->serializer ->expects($this->once()) ->method('serialize') @@ -227,7 +217,7 @@ public function testJsonWithSerializer() public function testJsonWithSerializerContextOverride() { - $this->serializer = $this->getMock(SerializerInterface::class); + $this->serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); $this->serializer ->expects($this->once()) ->method('serialize') @@ -244,7 +234,6 @@ public function testJsonWithSerializerContextOverride() public function testAddFlash() { $this->flashBag = new FlashBag(); - $this->addFlash('foo', 'bar'); $this->assertSame(array('bar'), $this->flashBag->get('foo')); @@ -267,50 +256,24 @@ public function testDenyAccessUnlessGranted() $this->denyAccessUnlessGranted('foo'); } - public function testRenderViewTemplating() - { - $this->templating = $this->getMock(EngineInterface::class); - $this->templating->expects($this->once())->method('render')->willReturn('bar'); - - $this->assertEquals('bar', $this->renderView('foo')); - } - public function testRenderViewTwig() { - $this->templating = false; $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); $this->twig->expects($this->once())->method('render')->willReturn('bar'); $this->assertEquals('bar', $this->renderView('foo')); } - public function testRenderTemplating() - { - $this->templating = $this->getMock(EngineInterface::class); - $this->templating->expects($this->once())->method('renderResponse')->willReturn(new Response('bar')); - - $this->assertEquals('bar', $this->render('foo')->getContent()); - } - public function testRenderTwig() { - $this->templating = false; $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); $this->twig->expects($this->once())->method('render')->willReturn('bar'); $this->assertEquals('bar', $this->render('foo')->getContent()); } - public function testStreamTemplating() - { - $this->templating = $this->getMock(EngineInterface::class); - - $this->assertInstanceOf(StreamedResponse::class, $this->stream('foo')); - } - public function testStreamTwig() { - $this->templating = false; $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); $this->assertInstanceOf(StreamedResponse::class, $this->stream('foo')); @@ -328,9 +291,9 @@ public function testCreateAccessDeniedException() public function testCreateForm() { - $form = $this->getMock(FormInterface::class); + $form = $this->getMockBuilder(FormInterface::class)->getMock(); - $this->formFactory = $this->getMock(FormFactoryInterface::class); + $this->formFactory = $this->getMockBuilder(FormFactoryInterface::class)->getMock(); $this->formFactory->expects($this->once())->method('create')->willReturn($form); $this->assertEquals($form, $this->createForm('foo')); @@ -338,9 +301,9 @@ public function testCreateForm() public function testCreateFormBuilder() { - $formBuilder = $this->getMock(FormBuilderInterface::class); + $formBuilder = $this->getMockBuilder(FormBuilderInterface::class)->getMock(); - $this->formFactory = $this->getMock(FormFactoryInterface::class); + $this->formFactory = $this->getMockBuilder(FormFactoryInterface::class)->getMock(); $this->formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder); $this->assertEquals($formBuilder, $this->createFormBuilder('foo')); From f1cb9fab03e96dd46a1664ea6d9ff9091961d183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 13 Feb 2017 12:26:04 +0100 Subject: [PATCH 4/8] Fix tests --- .../Controller/ControllerTrait.php | 2 +- .../Tests/Controller/ControllerTraitTest.php | 271 +++++++++--------- .../UseControllerTraitController.php | 182 ++++++++++++ 3 files changed, 321 insertions(+), 134 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Controller/UseControllerTraitController.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 316238fa5fefb..073de9c664952 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -48,7 +48,7 @@ trait ControllerTrait { protected function getRouter(): RouterInterface { - throw new \LogicException(sprintf('An instance of "%" must be provided.', RouterInterface::class)); + throw new \LogicException(sprintf('An instance of "%s" must be provided.', RouterInterface::class)); } protected function getRequestStack(): RequestStack diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index acd05ba283553..e2cfb604f243d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -12,7 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; use Doctrine\Common\Persistence\ManagerRegistry; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Controller\UseControllerTraitController; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; @@ -43,26 +43,18 @@ */ class ControllerTraitTest extends \PHPUnit_Framework_TestCase { - use ControllerTrait { - getSerializer as traitGetSerializer; - } - - private $token; - private $serializer; - private $flashBag; - private $isGranted; - private $twig; - private $formFactory; - - protected function getRouter() + public function testGenerateUrl() { $router = $this->getMockBuilder(RouterInterface::class)->getMock(); $router->expects($this->once())->method('generate')->willReturn('/foo'); - return $router; + $controller = new UseControllerTraitController(); + $controller->setRouter($router); + + $this->assertEquals('/foo', $controller->generateUrl('foo')); } - protected function getRequestStack() + public function testForward() { $request = Request::create('/'); $request->setLocale('fr'); @@ -71,92 +63,24 @@ protected function getRequestStack() $requestStack = new RequestStack(); $requestStack->push($request); - return $requestStack; - } - - protected function getHttpKernel() - { - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); - $kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { + $httpKernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $httpKernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { return new Response($request->getRequestFormat().'--'.$request->getLocale()); })); - return $kernel; - } - - protected function getSerializer() - { - if ($this->serializer) { - return $this->serializer; - } - - return $this->traitGetSerializer(); - } - - protected function getSession() - { - $session = $this->getMockBuilder(Session::class)->getMock(); - $session->expects($this->once())->method('getFlashBag')->willReturn($this->flashBag); - - return $session; - } - - protected function getAuthorizationChecker() - { - $authorizationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock(); - $authorizationChecker->expects($this->once())->method('isGranted')->willReturn($this->isGranted); - - return $authorizationChecker; - } - - protected function getTwig() - { - return $this->twig; - } - - protected function getDoctrine() - { - return $this->getMockBuilder(ManagerRegistry::class)->getMock(); - } - - protected function getFormFactory() - { - return $this->formFactory; - } - - protected function getTokenStorage() - { - $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); - $tokenStorage - ->expects($this->once()) - ->method('getToken') - ->will($this->returnValue($this->token)); - - return $tokenStorage; - } - - protected function getCsrfTokenManager() - { - $tokenManager = $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock(); - $tokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); - - return $tokenManager; - } + $controller = new UseControllerTraitController(); + $controller->setRequestStack($requestStack); + $controller->setHttpKernel($httpKernel); - public function testGenerateUrl() - { - $this->assertEquals('/foo', $this->generateUrl('foo')); - } - - public function testForward() - { - $response = $this->forward('a_controller'); + $response = $controller->forward('a_controller'); $this->assertEquals('xml--fr', $response->getContent()); } public function testRedirect() { - $response = $this->redirect('http://example.com', 301); + $controller = new UseControllerTraitController(); + + $response = $controller->redirect('http://example.com', 301); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertSame('http://example.com', $response->getTargetUrl()); @@ -165,7 +89,13 @@ public function testRedirect() public function testRedirectToRoute() { - $response = $this->redirectToRoute('foo'); + $router = $this->getMockBuilder(RouterInterface::class)->getMock(); + $router->expects($this->once())->method('generate')->willReturn('/foo'); + + $controller = new UseControllerTraitController(); + $controller->setRouter($router); + + $response = $controller->redirectToRoute('foo'); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertSame('/foo', $response->getTargetUrl()); @@ -175,56 +105,86 @@ public function testRedirectToRoute() public function testGetUser() { $user = new User('user', 'pass'); - $this->token = new UsernamePasswordToken($user, 'pass', 'default', array('ROLE_USER')); - $this->assertSame($this->getUser(), $user); + $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue(new UsernamePasswordToken($user, 'pass', 'default', array('ROLE_USER')))); + + $controller = new UseControllerTraitController(); + $controller->setTokenStorage($tokenStorage); + + $this->assertSame($controller->getUser(), $user); } public function testGetUserAnonymousUserConvertedToNull() { - $this->token = new AnonymousToken('default', 'anon.'); - $this->assertNull($this->getUser()); + $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue(new AnonymousToken('default', 'anon.'))); + + $controller = new UseControllerTraitController(); + $controller->setTokenStorage($tokenStorage); + + $this->assertNull($controller->getUser()); } public function testGetUserWithEmptyTokenStorage() { - $this->token = null; - $this->assertNull($this->getUser()); + $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue(null)); + + $controller = new UseControllerTraitController(); + $controller->setTokenStorage($tokenStorage); + + $this->assertNull($controller->getUser()); } public function testJson() { - $this->serializer = null; + $controller = new UseControllerTraitController(); - $response = $this->json(array()); + $response = $controller->json(array()); $this->assertInstanceOf(JsonResponse::class, $response); $this->assertEquals('[]', $response->getContent()); } public function testJsonWithSerializer() { - $this->serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); - $this->serializer + $serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); + $serializer ->expects($this->once()) ->method('serialize') ->with(array(), 'json', array('json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS)) ->will($this->returnValue('[]')); - $response = $this->json(array()); + $controller = new UseControllerTraitController(); + $controller->setSerializer($serializer); + + $response = $controller->json(array()); $this->assertInstanceOf(JsonResponse::class, $response); $this->assertEquals('[]', $response->getContent()); } public function testJsonWithSerializerContextOverride() { - $this->serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); - $this->serializer + $serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); + $serializer ->expects($this->once()) ->method('serialize') ->with(array(), 'json', array('json_encode_options' => 0, 'other' => 'context')) ->will($this->returnValue('[]')); - $response = $this->json(array(), 200, array(), array('json_encode_options' => 0, 'other' => 'context')); + $controller = new UseControllerTraitController(); + $controller->setSerializer($serializer); + + $response = $controller->json(array(), 200, array(), array('json_encode_options' => 0, 'other' => 'context')); $this->assertInstanceOf(JsonResponse::class, $response); $this->assertEquals('[]', $response->getContent()); $response->setEncodingOptions(JSON_FORCE_OBJECT); @@ -233,17 +193,28 @@ public function testJsonWithSerializerContextOverride() public function testAddFlash() { - $this->flashBag = new FlashBag(); - $this->addFlash('foo', 'bar'); + $flashBag = new FlashBag(); + + $session = $this->getMockBuilder(Session::class)->getMock(); + $session->method('getFlashBag')->willReturn($flashBag); - $this->assertSame(array('bar'), $this->flashBag->get('foo')); + $controller = new UseControllerTraitController(); + $controller->setSession($session); + + $controller->addFlash('foo', 'bar'); + + $this->assertSame(array('bar'), $flashBag->get('foo')); } public function testIsGranted() { - $this->isGranted = true; + $authorizationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock(); + $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true); - $this->assertTrue($this->isGranted('foo')); + $controller = new UseControllerTraitController(); + $controller->setAuthorizationChecker($authorizationChecker); + + $this->assertTrue($controller->isGranted('foo')); } /** @@ -251,71 +222,105 @@ public function testIsGranted() */ public function testDenyAccessUnlessGranted() { - $this->isGranted = false; + $authorizationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock(); + $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(false); + + $controller = new UseControllerTraitController(); + $controller->setAuthorizationChecker($authorizationChecker); - $this->denyAccessUnlessGranted('foo'); + $controller->denyAccessUnlessGranted('foo'); } - public function testRenderViewTwig() + public function testRenderView() { - $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); - $this->twig->expects($this->once())->method('render')->willReturn('bar'); + $twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); + $twig->expects($this->once())->method('render')->willReturn('bar'); - $this->assertEquals('bar', $this->renderView('foo')); + $controller = new UseControllerTraitController(); + $controller->setTwig($twig); + + $this->assertEquals('bar', $controller->renderView('foo')); } public function testRenderTwig() { - $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); - $this->twig->expects($this->once())->method('render')->willReturn('bar'); + $twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); + $twig->expects($this->once())->method('render')->willReturn('bar'); + + $controller = new UseControllerTraitController(); + $controller->setTwig($twig); - $this->assertEquals('bar', $this->render('foo')->getContent()); + $this->assertEquals('bar', $controller->render('foo')->getContent()); } public function testStreamTwig() { - $this->twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); + $twig = $this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(); + + $controller = new UseControllerTraitController(); + $controller->setTwig($twig); - $this->assertInstanceOf(StreamedResponse::class, $this->stream('foo')); + $this->assertInstanceOf(StreamedResponse::class, $controller->stream('foo')); } public function testCreateNotFoundException() { - $this->assertInstanceOf(NotFoundHttpException::class, $this->createNotFoundException()); + $controller = new UseControllerTraitController(); + + $this->assertInstanceOf(NotFoundHttpException::class, $controller->createNotFoundException()); } public function testCreateAccessDeniedException() { - $this->assertInstanceOf(AccessDeniedException::class, $this->createAccessDeniedException()); + $controller = new UseControllerTraitController(); + + $this->assertInstanceOf(AccessDeniedException::class, $controller->createAccessDeniedException()); } public function testCreateForm() { $form = $this->getMockBuilder(FormInterface::class)->getMock(); - $this->formFactory = $this->getMockBuilder(FormFactoryInterface::class)->getMock(); - $this->formFactory->expects($this->once())->method('create')->willReturn($form); + $formFactory = $this->getMockBuilder(FormFactoryInterface::class)->getMock(); + $formFactory->expects($this->once())->method('create')->willReturn($form); + + $controller = new UseControllerTraitController(); + $controller->setFormFactory($formFactory); - $this->assertEquals($form, $this->createForm('foo')); + $this->assertEquals($form, $controller->createForm('foo')); } public function testCreateFormBuilder() { $formBuilder = $this->getMockBuilder(FormBuilderInterface::class)->getMock(); - $this->formFactory = $this->getMockBuilder(FormFactoryInterface::class)->getMock(); - $this->formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder); + $formFactory = $this->getMockBuilder(FormFactoryInterface::class)->getMock(); + $formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder); - $this->assertEquals($formBuilder, $this->createFormBuilder('foo')); + $controller = new UseControllerTraitController(); + $controller->setFormFactory($formFactory); + + $this->assertEquals($formBuilder, $controller->createFormBuilder('foo')); } public function testGetDoctrine() { - $this->assertInstanceOf(ManagerRegistry::class, $this->getDoctrine()); + $doctrine = $this->getMockBuilder(ManagerRegistry::class)->getMock(); + + $controller = new UseControllerTraitController(); + $controller->setDoctrine($doctrine); + + $this->assertSame($doctrine, $controller->getDoctrine()); } public function testIsCsrfTokenValid() { - $this->assertTrue($this->isCsrfTokenValid('foo', 'bar')); + $csrfTokenManager = $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock(); + $csrfTokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); + + $controller = new UseControllerTraitController(); + $controller->setCsrfTokenManager($csrfTokenManager); + + $this->assertTrue($controller->isCsrfTokenValid('foo', 'bar')); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Controller/UseControllerTraitController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Controller/UseControllerTraitController.php new file mode 100644 index 0000000000000..aa53655a74e5c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Controller/UseControllerTraitController.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Controller; + +use Doctrine\Common\Persistence\ManagerRegistry; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * @author Kévin Dunglas + */ +class UseControllerTraitController +{ + use ControllerTrait { + getRouter as traitGetRouter; + getRequestStack as traitGetRequestStack; + getHttpKernel as traitGetHttpKernel; + getSerializer as traitGetSerializer; + getSession as traitGetSession; + getAuthorizationChecker as traitGetAuthorizationChecker; + getTwig as traitGetTwig; + getDoctrine as traitGetDoctrine; + getFormFactory as traitGetFormFactory; + getTokenStorage as traitGetTokenStorage; + getCsrfTokenManager as traitGetCsrfTokenManager; + + generateUrl as public; + forward as public; + redirect as public; + redirectToRoute as public; + json as public; + file as public; + addFlash as public; + isGranted as public; + denyAccessUnlessGranted as public; + renderView as public; + render as public; + stream as public; + createNotFoundException as public; + createAccessDeniedException as public; + createForm as public; + createFormBuilder as public; + getUser as public; + isCsrfTokenValid as public; + } + + private $router; + private $httpKernel; + private $serializer; + private $authorizationChecker; + private $session; + private $twig; + private $doctrine; + private $formFactory; + + public function setRouter(RouterInterface $router) + { + $this->router = $router; + } + + protected function getRouter(): RouterInterface + { + return $this->router ?? $this->traitGetRouter(); + } + + public function setRequestStack(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + + protected function getRequestStack(): RequestStack + { + return $this->requestStack ?? $this->traitGetRequestStack(); + } + + public function setHttpKernel(HttpKernelInterface $httpKernel) + { + $this->httpKernel = $httpKernel; + } + + protected function getHttpKernel(): HttpKernelInterface + { + return $this->httpKernel ?? $this->traitGetHttpKernel(); + } + + public function setSerializer(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } + + protected function getSerializer(): SerializerInterface + { + return $this->serializer ?? $this->traitGetSerializer(); + } + + public function setSession(Session $session) + { + $this->session = $session; + } + + protected function getSession(): Session + { + return $this->session ?? $this->traitGetSession(); + } + + public function setAuthorizationChecker(AuthorizationCheckerInterface $authorizationChecker) + { + $this->authorizationChecker = $authorizationChecker; + } + + protected function getAuthorizationChecker(): AuthorizationCheckerInterface + { + return $this->authorizationChecker ?? $this->traitGetAuthorizationChecker(); + } + + public function setTwig(\Twig_Environment $twig) + { + $this->twig = $twig; + } + + protected function getTwig(): \Twig_Environment + { + return $this->twig ?? $this->traitGetTwig(); + } + + public function setDoctrine(ManagerRegistry $doctrine) + { + $this->doctrine = $doctrine; + } + + public function getDoctrine(): ManagerRegistry + { + return $this->doctrine ?? $this->traitGetDoctrine(); + } + + public function setFormFactory(FormFactoryInterface $formFactory) + { + $this->formFactory = $formFactory; + } + + protected function getFormFactory(): FormFactoryInterface + { + return $this->formFactory ?? $this->traitGetFormFactory(); + } + + public function setTokenStorage(TokenStorageInterface $tokenStorage) + { + $this->tokenStorage = $tokenStorage; + } + + protected function getTokenStorage(): TokenStorageInterface + { + return $this->tokenStorage ?? $this->traitGetTokenStorage(); + } + + public function setCsrfTokenManager(CsrfTokenManagerInterface $csrfTokenManager) + { + $this->csrfTokenManager = $csrfTokenManager; + } + + protected function getCsrfTokenManager(): CsrfTokenManagerInterface + { + return $this->csrfTokenManager ?? $this->traitGetCsrfTokenManager(); + } +} From dcc7e09faecc512a794d2fdc8d7845a2def0d36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 1 Mar 2017 22:30:32 +0100 Subject: [PATCH 5/8] Add annotation --- .../Controller/ControllerTrait.php | 32 ++++++++++++++++++- .../Tests/Controller/ControllerTraitTest.php | 3 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 073de9c664952..1866e167a18e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -46,21 +46,33 @@ */ trait ControllerTrait { + /** + * @required + */ protected function getRouter(): RouterInterface { throw new \LogicException(sprintf('An instance of "%s" must be provided.', RouterInterface::class)); } + /** + * @required + */ protected function getRequestStack(): RequestStack { throw new \LogicException(sprintf('An instance of "%s" must be provided.', RequestStack::class)); } + /** + * @required + */ protected function getHttpKernel(): HttpKernelInterface { throw new \LogicException(sprintf('An instance of "%s" must be provided.', HttpKernelInterface::class)); } + /** + * @required + */ protected function getSerializer(): SerializerInterface { throw new \LogicException(sprintf('An instance of "%s" must be provided.', SerializerInterface::class)); @@ -70,38 +82,56 @@ protected function getSerializer(): SerializerInterface * An instance of the Session implementation (and not the interface) is returned because getFlashBag is not part of * the interface. * - * @return Session + * @required */ protected function getSession(): Session { throw new \LogicException(sprintf('An instance of "%s" must be provided.', Session::class)); } + /** + * @required + */ protected function getAuthorizationChecker(): AuthorizationCheckerInterface { throw new \LogicException(sprintf('An instance of "%s" must be provided.', AuthorizationCheckerInterface::class)); } + /** + * @required + */ protected function getTwig(): \Twig_Environment { throw new \LogicException(sprintf('An instance of "%s" must be provided.', \Twig_Environment::class)); } + /** + * @required + */ protected function getDoctrine(): ManagerRegistry { throw new \LogicException(sprintf('An instance of "%s" must be provided.', ManagerRegistry::class)); } + /** + * @required + */ protected function getFormFactory(): FormFactoryInterface { throw new \LogicException(sprintf('An instance of "%s" must be provided.', FormFactoryInterface::class)); } + /** + * @required + */ protected function getTokenStorage(): TokenStorageInterface { throw new \LogicException(sprintf('An instance of "%s" must be provided.', TokenStorageInterface::class)); } + /** + * @required + */ protected function getCsrfTokenManager(): CsrfTokenManagerInterface { throw new \LogicException(sprintf('An instance of "%s" must be provided.', CsrfTokenManagerInterface::class)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index e2cfb604f243d..1cf802be14452 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -35,13 +35,14 @@ use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; +use PHPUnit\Framework\TestCase as PHPUnitTestCase; /** * @author Kévin Dunglas * * @requires PHP 7 */ -class ControllerTraitTest extends \PHPUnit_Framework_TestCase +class ControllerTraitTest extends PHPUnitTestCase { public function testGenerateUrl() { From f2e54f660e90126824b0706653c222adb41fe9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 2 Mar 2017 20:46:46 +0100 Subject: [PATCH 6/8] Remove exceptions and the conditional behavior of json() --- .../Controller/ControllerTrait.php | 29 +++++-------------- .../Tests/Controller/ControllerTraitTest.php | 9 ------ 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 1866e167a18e2..44e7e67047bea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -51,7 +51,6 @@ trait ControllerTrait */ protected function getRouter(): RouterInterface { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', RouterInterface::class)); } /** @@ -59,7 +58,6 @@ protected function getRouter(): RouterInterface */ protected function getRequestStack(): RequestStack { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', RequestStack::class)); } /** @@ -67,7 +65,6 @@ protected function getRequestStack(): RequestStack */ protected function getHttpKernel(): HttpKernelInterface { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', HttpKernelInterface::class)); } /** @@ -75,7 +72,6 @@ protected function getHttpKernel(): HttpKernelInterface */ protected function getSerializer(): SerializerInterface { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', SerializerInterface::class)); } /** @@ -86,7 +82,6 @@ protected function getSerializer(): SerializerInterface */ protected function getSession(): Session { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', Session::class)); } /** @@ -94,7 +89,6 @@ protected function getSession(): Session */ protected function getAuthorizationChecker(): AuthorizationCheckerInterface { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', AuthorizationCheckerInterface::class)); } /** @@ -102,7 +96,6 @@ protected function getAuthorizationChecker(): AuthorizationCheckerInterface */ protected function getTwig(): \Twig_Environment { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', \Twig_Environment::class)); } /** @@ -110,7 +103,6 @@ protected function getTwig(): \Twig_Environment */ protected function getDoctrine(): ManagerRegistry { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', ManagerRegistry::class)); } /** @@ -118,7 +110,6 @@ protected function getDoctrine(): ManagerRegistry */ protected function getFormFactory(): FormFactoryInterface { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', FormFactoryInterface::class)); } /** @@ -126,7 +117,6 @@ protected function getFormFactory(): FormFactoryInterface */ protected function getTokenStorage(): TokenStorageInterface { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', TokenStorageInterface::class)); } /** @@ -134,7 +124,6 @@ protected function getTokenStorage(): TokenStorageInterface */ protected function getCsrfTokenManager(): CsrfTokenManagerInterface { - throw new \LogicException(sprintf('An instance of "%s" must be provided.', CsrfTokenManagerInterface::class)); } /** @@ -200,26 +189,22 @@ protected function redirectToRoute(string $route, array $parameters = array(), i } /** - * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. + * Returns a JsonResponse that uses the serializer component. * * @param mixed $data The response data * @param int $status The status code to use for the Response * @param array $headers Array of extra headers to add - * @param array $context Context to pass to serializer when using serializer component + * @param array $context Context to pass to serializer * * @return JsonResponse */ protected function json($data, int $status = 200, array $headers = array(), array $context = array()): JsonResponse { - try { - $json = $this->getSerializer()->serialize($data, 'json', array_merge(array( - 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, - ), $context)); - - return new JsonResponse($json, $status, $headers, true); - } catch (\LogicException $e) { - return new JsonResponse($data, $status, $headers); - } + $json = $this->getSerializer()->serialize($data, 'json', array_merge(array( + 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, + ), $context)); + + return new JsonResponse($json, $status, $headers, true); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index 1cf802be14452..b39eeab98066c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -147,15 +147,6 @@ public function testGetUserWithEmptyTokenStorage() $this->assertNull($controller->getUser()); } - public function testJson() - { - $controller = new UseControllerTraitController(); - - $response = $controller->json(array()); - $this->assertInstanceOf(JsonResponse::class, $response); - $this->assertEquals('[]', $response->getContent()); - } - public function testJsonWithSerializer() { $serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); From 662eac4ffcac745d05306afa0395a31afad1cdbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 2 Mar 2017 20:52:58 +0100 Subject: [PATCH 7/8] Add author --- .../Bundle/FrameworkBundle/Controller/ControllerTrait.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 44e7e67047bea..ac4bd7fdb0678 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -41,6 +41,7 @@ * The recommended way of injecting dependencies is trough getter injection. * * @author Kévin Dunglas + * @author Fabien Potencier * * @experimental in version 3.3 */ From 8972503aa134fc98d9fdcebd41f20af1af408951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 2 Mar 2017 21:08:01 +0100 Subject: [PATCH 8/8] Add a mention in CHANGELOG --- src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md | 6 ++++++ .../Bundle/FrameworkBundle/Controller/ControllerTrait.php | 2 +- .../Tests/Controller/ControllerTraitTest.php | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 9f8061adc46f3..d09196908fb31 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -21,6 +21,12 @@ CHANGELOG Use `Symfony\Component\Console\DependencyInjection\ConfigCachePass` instead. * Deprecated `PropertyInfoPass`, use `Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass` instead * Deprecated extending `ConstraintValidatorFactory` + * Added `Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait` (requires PHP 7). Unlike the `Symfony\Bundle\FrameworkBundle\Controller\Controller` + class, this trait does not have access to the dependency injection container. Its dependencies are explicitly and lazily + injected using getter injection. + `render()`, `renderView()` and `stream()` methods can only use Twig (using the Templating component is not supported). + The `json()` method requires the Serializer component (use `Symfony\Component\HttpFoundation\JsonResponse` directly if + you do not want to use the Serializer). 3.2.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index ac4bd7fdb0678..0f2604f486932 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; use Doctrine\Common\Persistence\ManagerRegistry; +use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; @@ -30,7 +31,6 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Csrf\CsrfToken; -use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index b39eeab98066c..ccbd50794beff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; use Doctrine\Common\Persistence\ManagerRegistry; +use PHPUnit\Framework\TestCase as PHPUnitTestCase; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Controller\UseControllerTraitController; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; @@ -35,7 +36,6 @@ use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; -use PHPUnit\Framework\TestCase as PHPUnitTestCase; /** * @author Kévin Dunglas