I am working on an ExceptionListener and for some controllers I want errors to be formatted as json responses. I thought I would define an option in #Route annotation and then use it in ExceptionListener:
/**
* #Route("/route/path", name="route_name", options={"response_type": "json"})
*/
and:
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// ...
}
}
but GetResponseForExceptionEvent doesn't contain any info about matched route. Is there a way to get the options array inside ExceptionListener?
thanks.
You should be able to retrieve the route name from the attribute request with
$request = $event->getRequest();
$routeName = $request->attributes->get('_route');
then, if you inject the router service into your class, you can get the instance of the route with
$route = $this->router->getRouteCollection()->get($routeName);
finally
$options = $route->getOptions();
echo $options['response_type']
use Symfony\Component\Routing\RouterInterface;
class ExceptionListener
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
$route = $this->router->getRouteCollection()->get(
$request->attributes->get('_route')
);
$options = $route->getOptions();
// $options['response_type'];
}
}
Related
In /src/AppBundle/Controller/CustomExceptionController.php I have:
namespace AppBundle\Controller;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CustomExceptionController extends \Symfony\Bundle\TwigBundle\Controller\ExceptionController
{
public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
{
return $this->redirectToRoute('custom_error'); //not working
}
}
This not working because \Symfony\Bundle\TwigBundle\Controller\ExceptionController not extends class Controller. So how can I use $this->redirectToRoute in this class?
redirectToRoute is part of the Controller class like you mentioned.
All you need to do is create the method yourself.
First you'll need to inject the router into your CustomExceptionController (therefore you need to define your custom controller as a service in the DI)
services:
my.custom.exception_controller:
class: CustomExceptionController
arguments: [ "#twig", "%kernel.debug%", "#router" ]
twig:
exception_controller: my.custom.exception_controller:showAction
Your custom class should look like this:
class CustomExceptionController extends \Symfony\Bundle\TwigBundle\Controller\ExceptionController
{
protected $router;
public function __construct(\Twig_Environment $twig, $debug, Router $router)
{
parent::__construct($twig, $debug);
$this->router = $router;
}
public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
{
}
}
After that you can implement the redirectToRoute in the CustomExceptionController just like it's done in the Controller (or just create the RedirectResponse without the helper methods directly)
/**
* 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
*/
public 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->router->generateUrl($route, $parameters), $status);
}
easy way with using UrlGeneratorInterface like
getContainer()->get('router')->generate( 'custom_error', ['key' => 'some val'], 0 );
getContainer() - is own funct, see manual If you need to generate a URL from a service, type-hint the UrlGeneratorInterface service:
// src/Service/SomeService.php
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SomeService
{
private $router;
public function __construct(UrlGeneratorInterface $router)
{
$this->router = $router;
}
public function someMethod()
{
$url = $this->router->generate( 'custom_error', [ 'key' => 'some value' ] );
}
}
I need to access a $_POST variable from a service and I don't want to pass request as a paramter as I think this forces me to use scope: request in the service and I have bad memories from this scope from the past, as this forces a service instance per request, instead of one instance per application.
The thing is that $_POST works all right, but as symfony best practices recommends to avoid using php primitives I ask if there's a better way to do it (avoiding scope: request in service)
If you are using 2.4+ you can use the request_stack, see here.
You would use it like..
services.yml
your.service:
class: FQCN\To\Your\Service
arguments:
- #request_stack
FQCN\To\Your\Service
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class Service
{
/**
* #var RequestStack
*/
private $requestStack;
/**
* #var Request
*/
private $request;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function yourCall()
{
$param = $this->getRequest()->request->get('your-post-parameter');
//...
}
/**
* Get current request object
*
* #return Request
*/
private function getRequest()
{
if (null === $this->request) {
$this->request = $this->requestStack->getCurrentRequest();
}
return $this->request;
}
}
I don't see any problem with doing it like below which is what I did some time ago. You're not injecting whole Request.
CONTROLLER
public function searchByGetAction(Request $request)
{
//.......
$results = $this->productSearchService->findWithSimpleArray(
$request->query
);
//.......
}
public function searchByPostAction(Request $request)
{
//.......
$results = $this->productSearchService->findkWithNestedArray(
json_decode($request->getContent(), true)
);
//.......
}
SERVICE
public function findWithSimpleArray(ParameterBag $searchParameters)
{
$name = $searchParameters->get('name');
$surname = $searchParameters->get('surname');
//.......
}
public function findWithNestedArray($searchParameters = [])
{
$name = isset($searchParameters['name']) ? $searchParameters['name'] : null;
$surname = isset($searchParameters['surname']) ? $searchParameters['surname'] : null;
//.......
}
I need to do a custom isGranted method (not using Rbac or acl module from community). So I have a service which provides the functionality. But this code:
if (!$this->userService->isGrantedCustom($this->session->offsetGet('cod_lvl'), 'ZF_INV_HOM')) {
throw new \Exception("you_are_not_allowed", 1);
}
...is duplicated in each controller and each action I have. Parameters are changing of course depends on the permission ('ZF_INV_HOM', 'ZF_TODO_DELETE' ...).
I think it's not a bad idea to do this code before the controller is called, but I can't figure what is the best solution (best architecture), and how to pass those parameters to it (I thought about annotation on controllers but how to handle this ?).
The point is, if I have to modify this code I can't imagine to do that hundreds of times, for each controllers, each action I have I need to have this code in one place.
If you don't want to pollute your Module with all this code you can also make a listener class and attach only the listener in your bootstrap method:
<?php
namespace Application\Listener;
use Application\Service\UserService;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\SharedEventManagerInterface;
use Zend\EventManager\SharedListenerAggregateInterface;
use Zend\Authentication\AuthenticationServiceInterface;
class IsAllowedListener implements SharedListenerAggregateInterface
{
/**
* #var AuthenticationServiceInterface
*/
protected $authService;
/**
* #var UserService
*/
protected $userService;
/**
* #var \Zend\Stdlib\CallbackHandler[]
*/
protected $sharedListeners = array();
/**
* #param SharedEventManagerInterface $events
*/
public function attachShared(SharedEventManagerInterface $events)
{
$this->sharedListeners[] = $events->attach(AbstractActionController::class, MvcEvent::EVENT_DISPATCH, array($this, 'isAllowed'), 1000);
}
public function __construct(AuthenticationServiceInterface $authService, UserService $userService ){
$this->authService = $authService;
$this->userService = $userService;
}
/**
* #param MvcEvent $event
*/
protected function isAllowed(MvcEvent $event)
{
$authService = $this->getAuthService();
$identity = $authService->getIdentity();
$userService = $this->getUserService();
if($userService->isGrantedCustom()){
// User is granted we can return
return;
}
// Return not allowed response
}
/**
* #return AuthenticationServiceInterface
*/
public function getAuthService()
{
return $this->authService;
}
/**
* #param AuthenticationServiceInterface $authService
*/
public function setAuthService(AuthenticationServiceInterface $authService)
{
$this->authService = $authService;
}
/**
* #return UserService
*/
public function getUserService()
{
return $this->userService;
}
/**
* #param UserService $userService
*/
public function setUserService(AuthenticationServiceInterface $userService)
{
$this->userService = $userService;
}
}
You need to setup a factory to inject your dependencies:
<?php
namespace Application\Listener;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
* Factory for creating the IsAllowedListener
*/
class IsAllowedListenerFactory implements FactoryInterface
{
/**
* Create the IsAllowedListener
*
* #param ServiceLocatorInterface $serviceLocator
* #return RenderLinksListener
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$userService = $serviceLocator->get('Application\Service\UserService');
return new IsAllowedListener($authService, $userService );
}
}
And register all this in config:
'service_manager' => array(
'factories' => array(
'Application\Listener\IsAllowedListener' => 'Application\Listener\IsAllowedListenerFactory'
)
)
And then in bootstrap:
public function onBootstrap(EventInterface $event)
{
$application = $event->getTarget();
$serviceManager = $application->getServiceManager();
$eventManager = $application->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
$isAllowedListener = $serviceManager->get('Application\Listener\IsAllowedListener')
$sharedEventManager->attachAggregate($isAllowedListener);
}
Instead of using AbstractActionController::class, you could also make a specific class, so you will only listen to instances of that class.
So for example AbstractIsAllowedActionController::class or something like that.
By attaching an event listener to the SharedEventManager you can target all controllers and have the authorization check in just one place.
In this case the target is Zend\Mvc\Controller\AbstractActionController which means any controller extending it will execute the listener. The high priority of this listener will mean that it is executed prior to the target controller action, giving you the chance to handle any requests that have not been authorized.
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager()->getSharedManager();
$eventManager->attach(
\Zend\Mvc\Controller\AbstractActionController::class, // Identity of the target controller
MvcEvent::EVENT_DISPATCH,
[$this, 'isAllowed'],
1000 // high priority
);
}
In each controller there would need to be some way that you can determine which 'resource' is being accessed.
As an example it could implement this interface
interface ResourceInterface
{
// Return a unique key representing the resource
public function getResourceId();
}
The listener could then look like this.
public function isAllowed(MvcEvent $event)
{
$serviceManager = $event->getApplication()->getServiceManager();
// We need the 'current' user identity
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$identity = $authService->getIdentity();
// The service that performs the authorization
$userService = $serviceManager->get('MyModule\Service\UserService');
// The target controller is itself a resource (the thing we want to access)
// in this example it returns an resource id so we know what we want to access
// but you could also get this 'id' from the request or config etc
$controller = $event->getTarget();
if ($controller instanceof ResourceInterface) {
$resourceName = $controller->getResourceId();
// Test the authorization, is UserX allowed resource ID Y
if (empty($resourceName) || $userService->isGrantedCustom($identity, $resourceName)) {
// early exit for success
return;
} else {
// Denied; perhaps trigger a new custom event or return a response
}
}
}
I'm trying to get the access_control parameters which are located in my security.yml as an array in my custom service.
Just like with getting the role_hierarchy parameters I thought it would work with the following code:
$accessParameters = $this->container->getParameter('security.access_control');
Unfortunately this was not the case. Can someone tell how to get the parameters?
There's no way to get the access_control parameter from the container.
This is because this parameter is only used to create request matchers which will be registered as AccessMap later given in the AccessListener, and then are left over without registering it into the container.
You can try something hacky to get these matchers back by getting them like
$context = $this->get("security.firewall.map.context.main")->getContext();
$listener = $context[0][5];
// Do reflection on "map" private member
But this is kind of an ugly solution.
Another way I can see on how to get them is to parse again the security file
use Symfony\Component\Yaml\Yaml;
$file = sprintf("%s/config/security.yml", $this->container->getParameter('kernel.root_dir'));
$parsed = Yaml::parse(file_get_contents($file));
$access = $parsed['security']['access_control'];
If you want to register this configuration into a service, you can do something like
services.yml
services:
acme.config_provider:
class: Acme\FooBundle\ConfigProvider
arguments:
- "%kernel.root_dir%"
acme.my_service:
class: Acme\FooBundle\MyService
arguments:
- "#acme.config_provider"
Acme\FooBundle\ConfigProvider
use Symfony\Component\Yaml\Yaml;
class ConfigProvider
{
protected $rootDir;
public function __construct($rootDir)
{
$this->rootDir = $rootDir;
}
public function getConfiguration()
{
$file = sprintf(
"%s/config/security.yml",
$this->rootDir
);
$parsed = Yaml::parse(file_get_contents($file));
return $parsed['security']['access_control'];
}
}
Acme\FooBundle\MyService
class MyService
{
protected $provider;
public function __construct(ConfigProvider $provider)
{
$this->provider = $provider;
}
public function doAction()
{
$access = $this->provider->getConfiguration();
foreach ($access as $line) {
// ...
}
}
}
Necro, but still relevant. This is an improvement on Touki's answer above, where we don't reparse the access_control definitions, but rather use the already configured security token, firewall and access map to work out the answer.
.../services.yml
...
My\Application\AuthenticationBundle\Security\AccessControlHelper:
class: My\Application\AuthenticationBundle\Security\AccessControlHelper
arguments:
$securityContext: "#security.context"
$firewall: '#security.firewall.map'
$accessDecisionManager: '#security.access.decision_manager'
$accessMap: '#security.access_map'
...
src/My/Application/AuthenticationBundle/Security/AccessControlHelper.php
declare(strict_types=1);
namespace My\Application\AuthenticationBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\Security\Http\FirewallMapInterface;
class AccessControlHelper
{
/**
* #var SecurityContextInterface
*/
protected $securityContext;
/**
* #var FirewallMapInterface
*/
protected $firewallMap;
/**
* #var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
/**
* #var AccessMapInterface
*/
protected $accessMap;
public function __construct(
SecurityContextInterface $securityContext,
FirewallMapInterface $firewallMap,
AccessDecisionManagerInterface $accessDecisionManager,
AccessMapInterface $accessMap
)
{
$this->securityContext = $securityContext;
$this->firewallMap = $firewallMap;
$this->accessDecisionManager = $accessDecisionManager;
$this->accessMap = $accessMap;
}
public function isRequestAccessible(Request $request): bool
{
$token = $this->securityContext->getToken();
if (!$token || false == $token->isAuthenticated()) {
return false;
}
list($listeners) = $this->firewallMap->getListeners($request);
if ($listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof AccessListener) {
/**
* Logic here is much inspired by the AccessListener->handle(...) method.
*/
list($attributes) = $this->accessMap->getPatterns($request);
if (null === $attributes) {
continue;
}
return boolval($this->accessDecisionManager->decide($token, $attributes, $request));
}
}
}
return true;
}
public function isUriAccessible(string $uri)
{
return $this->isRequestAccessible(Request::create($uri));
}
}
Sample usage:
use My\Application\AuthenticationBundle\Security\AccessControlHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
$container = ...; // #var ContainerInterface
$accessControlHelper = $container->get(AccessControlHelper::class);
$accessControlHelper->isRequestAccessible(new Request("/foo"));
$accessControlHelper->isUriAccessible("/foo");
I need to add a function in Symfony2 that has to be called on each request. (language detection on requestion & session)
I thought to do this in the constructor of my Controller classes, but there the container is not known / created.
Have you suggestions for this?
You can define your Event Listener
Please, read documentation about event listeners creation.
Here is a listener that redirects to a page with the language set in the user configuration. Adapt it to your needs.
<?php
namespace MyVendor\Listener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Routing\RouterInterface;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Observe;
/**
* #Service
*/
class LanguageListener
{
/**
* #var \Symfony\Component\Security\Core\SecurityContextInterface
*/
private $securityContext;
/**
* #var \Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #InjectParams
*
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
* #param \Symfony\Component\Routing\RouterInterface $router
*/
public function __construct(
SecurityContextInterface $securityContext,
RouterInterface $router
) {
$this->securityContext = $securityContext;
$this->router = $router;
}
/**
* #Observe("kernel.request")
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function forceLanguage(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$token = $this->securityContext->getToken();
if (!$token) {
return;
}
if (!$this->securityContext->isGranted('ROLE_USER')) {
return;
}
/** #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
$locale = $request->getLocale();
$route = $request->get('_route');
if ('_' === $route[0]) {
return;
}
/** #var $user \MyVendor\Model\User */
$user = $token->getUser();
if ($user->getConfig()->getLanguage() !== $locale) {
$parameters = array_merge($request->attributes->get('_route_params'), [
'_locale' => $user->getConfig()->getLanguage(),
]);
$path = $this->router->generate($route, $parameters);
$event->setResponse(new RedirectResponse($path));
}
}
}
I believe that it depends on what you are trying to do. For language detection most of the time symfony and its bundles handle virtually everything. That means that if you want to customize the routing you have to extend the routing component by using routing.loader tag..
However if you can use event listeners but I am not sure how many stuff you can change from there.
Either use events as suggested above or if you need something quick.
You can override setContainer method.
namespace My\Namespace;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MyController extends Controller
{
private $foo;
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer($container);
$this->foo = 'bar';
}
// your actions
}