Get route path '/blog/{slug}' of current request in Symfony - symfony

For some logging/monitoring i would like to get the current route path including placeholders.
If my route is /blog/{slug} and the request is to http://localhost/blog/foobar what i need is "/blog/{slug}"
In request listeners this value seem to not be inside the request object. I only find the resolved path which i am not interested in.
In Compiler passes I have the issue that any Router related service I try to get from the ContainerBuilder returns an exception. If i had the
What is a clean way to obtain this?

For routes with one param:
$routeWithoutParam = substr($request->getRequestUri(),0,strrpos($request->getRequestUri(),'/'));
$routeParams = $request->attributes->get('_route_params');
$routeDefinition = $routeWithoutParam.'/{' . array_key_first($paramsArray) . '}';
echo $routeDefinition;
For routes with multiple params:
$routeParams = $request->attributes->get('_route_params');
$routeWithoutParam = '';
for ($i = 0; $i < count($routeParams); $i++) {
$routeWithoutParam = $i === 0
? substr($request->getRequestUri(), 0, strrpos($request->getRequestUri(), '/'))
: substr($routeWithoutParam, 0, strrpos($routeWithoutParam, '/'))
;
}
$routeDefinition = $routeWithoutParam.'/';
foreach (array_keys($routeParams) as $key => $param) {
$routeDefinition.= '{' . $param . '}' . ($key < count($routeParams)-1 ? '/' : '');
}
echo $routeDefinition;

You can obtain the Router and RouteCollection, that holds all the routes in your app:
// src/Listener/RouteLoggerListener.php -- namespace and use ommited
class RouteLoggerListener implements EventSubscriberInterface
{
/**
* #var LoggerInterface
*/
private $logger;
/**
* #var RouterInterface
*/
private $router;
public function __construct(LoggerInterface $logger, RouterInterface $router)
{
$this->logger = $logger;
$this->router = $router;
}
public static function getSubscribedEvents()
{
// Trigger after the RouterListener
return [KernelEvents::REQUEST => ['onKernelRequest', 50]];
}
public function onKernelRequest(RequestEvent $event)
{
// This is for sf53 and up, for previous versions, use isMasterRequest()
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
if (null == $request) {
return;
}
$matchedRoute = $request->attributes->get('_route');
if (null == $matchedRoute) {
// Bail gracefully
return;
}
$routeCollection = $this->router->getRouteCollection();
$route = $routeCollection->get($matchedRoute);
$this->logger->debug('Request route: '.$route->getPath());
}
}
Note: Using RouteCollection at runtime is highly discouraged because it triggers a recompile. As indicated in this GitHub comment, the proper way to do it would be to use ConfigCache while cache warming.

Related

How to toggle Symfony's php-translation/symfony-bundle EditInPlace

I followed this documentation for Edit In Place, and setup the Activator, and it works!
However, I will be using this on the production site and allowing access via a ROLE_TRANSLATOR Authorization. This is also working, but I don't want the web interface always "on"
How would I go about enabling it via some sort of link or toggle?
My thoughts, it would be simple to just add a URL parameter, like ?trans=yes and then in the activator;
return ($this->authorizationChecker->isGranted(['ROLE_TRANSLATOR']) && $_GET['trans'] == 'yes');
Obviously, $_GET would not work, I didn't even try.
How do I generate a link to simply reload THIS page with the extra URL parameter
How do I check for that parameter within the "Activator"
or, is there a better way?
The "proper" way to do this, as I have discovered more about "services" is to do the logic diectly in the RoleActivator.php file.
referencing documentation for How to Inject Variables into all Templates via Referencing Services I came up with the following solution;
src/Security/RoleActivator.php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Translation\Bundle\EditInPlace\ActivatorInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class RoleActivator implements ActivatorInterface
{
/**
* #var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* #var TranslatorInterface
*/
private $translate;
/**
* #var RequestStack
*/
private $request;
private $params;
private $path;
private $flag = null;
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translate, RequestStack $request)
{
$this->authorizationChecker = $authorizationChecker;
$this->translate = $translate;
$this->request = $request;
}
/**
* {#inheritdoc}
*/
public function checkRequest(Request $request = null)
{
if ($this->flag === null) { $this->setFlag($request); }
try {
return ($this->authorizationChecker->isGranted(['ROLE_TRANSLATOR']) && $this->flag);
} catch (AuthenticationCredentialsNotFoundException $e) {
return false;
}
}
public function getText()
{
if ($this->flag === null) { $this->setFlag(); }
return ($this->flag) ? 'linkText.translate.finished' : 'linkText.translate.start'; // Translation key's returned
}
public function getHref()
{
if ($this->flag === null) { $this->setFlag(); }
$params = $this->params;
if ($this->flag) {
unset($params['trans']);
} else {
$params['trans'] = 'do';
}
$queryString = '';
if (!empty($params)) {
$queryString = '?';
foreach ($params as $key => $value) {
$queryString.= $key.'='.$value.'&';
}
$queryString = rtrim($queryString, '&');
}
return $this->path.$queryString;
}
private function setFlag(Request $request = null)
{
if ($request === null) {
$request = $this->request->getCurrentRequest();
}
$this->flag = $request->query->has('trans');
$this->params = $request->query->all();
$this->path = $request->getPathInfo();
}
}
config\packages\twig.yaml
twig:
# ...
globals:
EditInPlace: '#EditInPlace_RoleActivator'
config\services.yaml
services:
# ...
EditInPlace_RoleActivator:
class: App\Security\RoleActivator
arguments: ["#security.authorization_checker"]
So What I added over and above the php-translation example is the getText and getHref methods and corresponding private variables being set in the checkRequest and read there after.
Now in my twig template (in the header) I just use
{% if is_granted('ROLE_TRANSLATOR') %}
{{ EditInPlace.Text }}
{% endif %}
Add the new keys to the translation files and your done. the trans=do query parameter is toggled on and off with each click of the link. You could even add toggling styles with a class name, just copy the getText method to something like getClass and return string a or b with the Ternary.

Overriding FOSUserBundle loginAction() to redirect to custom routes

I am working on a Symfony 3.3.8 project with FOSUserBundle. I have created two registration and two profile pages for student and provider respectively. Now I am trying to override the FOSUserBundle's loginAction() method, where I am checking the logged-in user for it's role. If the role is ROLE_STUDENT I am trying to redirect the user to student profile page after successful login and if the role is ROLE_PROVIDER I want the user to be redirected to the provider profile page after successful login. Here is my overridden SecurityController with loginAction():
<?php
namespace Epita\HousingBundle\Controller;
use FOS\UserBundle\Controller\SecurityController as BaseController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class SecurityController extends BaseController {
/**
*
*/
public function loginAction(Request $request) {
/** #var $session \Symfony\Component\HttpFoundation\Session\Session */
$session = $request->getSession();
$key = '_security.main.target_path';
$securityContext = $this->container->get('security.authorization_checker');
if ($securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
$user = $this->getUser();
}
if ($this->container->get('session')->has($key))
{
if ($this->get('security.authorization_checker')->isGranted('ROLE_STUDENT')) {
return $this->redirectToRoute('student_profile');
} else if ($this->get('security.authorization_checker')->isGranted('ROLE_PROVIDER')) {
return $this->redirectToRoute('provider_profile');
}
}
else{
return $this->redirectToRoute('student_profile');
}
if (class_exists('\Symfony\Component\Security\Core\Security')) {
$authErrorKey = Security::AUTHENTICATION_ERROR;
$lastUsernameKey = Security::LAST_USERNAME;
} else {
// BC for SF < 2.6
$authErrorKey = SecurityContextInterface::AUTHENTICATION_ERROR;
$lastUsernameKey = SecurityContextInterface::LAST_USERNAME;
}
// get the error if any (works with forward and redirect -- see below)
if ($request->attributes->has($authErrorKey)) {
$error = $request->attributes->get($authErrorKey);
} elseif (null !== $session && $session->has($authErrorKey)) {
$error = $session->get($authErrorKey);
$session->remove($authErrorKey);
} else {
$error = null;
}
if (!$error instanceof AuthenticationException) {
$error = null; // The value does not come from the security component.
}
// last username entered by the user
$lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);
if ($this->has('security.csrf.token_manager')) {
$csrfToken = $this->get('security.csrf.token_manager')->getToken('authenticate')->getValue();
} else {
// BC for SF < 2.4
$csrfToken = $this->has('form.csrf_provider')
? $this->get('form.csrf_provider')->generateCsrfToken('authenticate')
: null;
}
return $this->renderLogin(array(
'last_username' => $lastUsername,
'error' => $error,
'csrf_token' => $csrfToken,
));
}
/**
*
* #param array $data
*
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function renderLogin(array $data)
{
$template = sprintf('EpitaHousingBundle:Security:login.html.twig');
return $this->container->get('templating')->renderResponse($template, $data);
}
public function checkAction()
{
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
}
public function logoutAction()
{
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
}
}
But this solution does not work for me. My guess is I have to play around with the session. Since I am new to this can anyone help me?
Thanks in advance. Let me know if you need more details.
Here is some code that might help you.
1 - This is the Event/LoginSuccessHandler, your logic is in this class
<?php
namespace AppBundle\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
protected $router;
protected $security;
public function __construct(Router $router, AuthorizationChecker $security)
{
$this->router = $router;
$this->security = $security;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$url = 'homepage';
if ($this->security->isGranted('ROLE_STUDENT')) {
$url = 'student_route';
}
if ($this->security->isGranted('ROLE_PROVIDER')) {
$url = 'provider_route';
}
$response = new RedirectResponse($this->router->generate($url));
return $response;
}
}
2 - We set up the config that listens the login event in your services.yml
login_success_handler:
class: AppBundle\Event\LoginSuccessHandler
arguments:
- "#router"
- "#security.authorization_checker"
tags:
- { name: 'monolog.logger', channel: 'security' }
3 - That should be it. Try and tell me if something is wrong

Symfony2 kernel exception event not handling fatal error exception in production mode

I make a listener for exception handling. Below is my code
services.yml
kernel.listener.prod_exception_listener:
class: MyBundle\Listener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
ExceptionListener.php
<?php
namespace MyBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// no fatal exception goes here others are coming in this function
// like 403,404,500 are coming in this block
}
}
What additional work I need to do for fatal exceptions in production mode? Because in dev mode fatal errors are coming in listener.
I solved it the following way,
in my services.yml
api_exception_subscriber:
class: AppBundle\EventListener\ApiExceptionSubscriber
arguments: ['%kernel.debug%', '#api.response_factory', '#logger']
tags:
- { name: kernel.event_subscriber }
api.response_factory:
class: AppBundle\Api\ResponseFactory
my response factory look like:
<?php
namespace AppBundle\Api;
use Symfony\Component\HttpFoundation\JsonResponse;
class ResponseFactory
{
public function createResponse(ApiProblem $apiProblem)
{
$data = $apiProblem->toArray();
$response = new JsonResponse(
$data,
$apiProblem->getStatusCode()
);
$response->headers->set('Content-Type', 'application/json');
return $response;
}
}
and the Api subscriper class
<?php
namespace AppBundle\EventListener;
use AppBundle\Api\ApiProblem;
use AppBundle\Api\ApiProblemException;
use AppBundle\Api\ResponseFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class ApiExceptionSubscriber implements EventSubscriberInterface
{
private $debug;
private $responseFactory;
private $logger;
public function __construct($debug, ResponseFactory $responseFactory, LoggerInterface $logger)
{
$this->debug = $debug;
$this->responseFactory = $responseFactory;
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
// only reply to /api URLs
if (strpos($event->getRequest()->getPathInfo(), '/api') !== 0) {
return;
}
$e = $event->getException();
$statusCode = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
// allow 500 errors to be thrown
if ($this->debug && $statusCode >= 500) {
return;
}
$this->logException($e);
if ($e instanceof ApiProblemException) {
$apiProblem = $e->getApiProblem();
} else {
$apiProblem = new ApiProblem(
$statusCode
);
/*
* If it's an HttpException message (e.g. for 404, 403),
* we'll say as a rule that the exception message is safe
* for the client. Otherwise, it could be some sensitive
* low-level exception, which should *not* be exposed
*/
if ($e instanceof HttpExceptionInterface) {
$apiProblem->set('detail', $e->getMessage());
}
}
$response = $this->responseFactory->createResponse($apiProblem);
$event->setResponse($response);
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::EXCEPTION => 'onKernelException'
);
}
/**
* Adapted from the core Symfony exception handling in ExceptionListener
*
* #param \Exception $exception
*/
private function logException(\Exception $exception)
{
$message = sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine());
$isCritical = !$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500;
$context = array('exception' => $exception);
if ($isCritical) {
$this->logger->critical($message, $context);
} else {
$this->logger->error($message, $context);
}
}
}
Edit 2020: as of Symfony 5 this is no longer necessary
I have handled this by overriding Kernel::handle to call the ExceptionListener manually
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true): Response
{
try {
return parent::handle($request, $type, $catch);
} catch (\Exception $exception) {
throw new \Exception("There was an issue booting the framework");
} catch (\Throwable $throwable) {
$exception = new FatalThrowableError($throwable);
$event = new ExceptionEvent($this, $request, $type, $exception);
/** #var ExceptionListener $exceptionListener */
$exceptionListener = $this->container->get(ExceptionListener::class);
$exceptionListener->onKernelException($event);
return $event->getResponse();
}
}

Symfony2: Get controller as a service classname from service name or Route object

I have some Controllers defined as services and I need to have the classname of my controllers from the route name.
For non-service controllers I can get the route collection with the router service:
$route = $this->router->getRouteCollection()->get($routeName);
//Retrieve an object like that:
Route {
-path: "/admin/dashboard"
-host: ""
-schemes: []
-methods: []
-defaults: array:1 [
"_controller" => "AppBundle\Controller\Admin\AdminController::dashboardAction"
]
-requirements: []
-options: array:1 []
-compiled: null
-condition: ""
}
I can access the controller classname with $route["defaults"]["_controller"] so this is fine.
The issue is with my controllers as services, the _controller attribute is the name of the service, not the Controller class (like app.controller.admin.user:listAction) I have the name of the service but I need to have the classname (AppBundle\Controller\Admin\UserController)
The only solution I came up with is to get the service from the Container and use get_class() on the service but it will have a huge performance impact only to retrieve the class of the controller/service.
Is there any other solution ?
as suggested in https://github.com/FriendsOfSymfony/FOSUserBundle/issues/2751, I implemented a cached map to get route-names resolved to controllers classes and methods.
<?php
// src/Cache/RouteClassMapWarmer.php
namespace App\Cache;
use Symfony\Component\Cache\Simple\PhpFilesCache;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\Routing\RouterInterface;
class RouteClassMapWarmer implements CacheWarmerInterface
{
/** #var ContainerInterface */
protected $container;
/** #var RouterInterface */
protected $router;
public function __construct(ContainerInterface $container, RouterInterface $router)
{
$this->container = $container;
$this->router = $router;
}
public function warmUp($cacheDirectory)
{
$cache = new PhpFilesCache('route_class_map', 0, $cacheDirectory);
$controllers = [];
foreach ($this->router->getRouteCollection() as $routeName => $route) {
$controller = $route->getDefault('_controller');
if (false === strpos($controller, '::')) {
list($controllerClass, $controllerMethod) = explode(':', $controller, 2);
// service_id gets resolved here
$controllerClass = get_class($this->container->get($controllerClass));
}
else {
list($controllerClass, $controllerMethod) = explode('::', $controller, 2);
}
$controllers[$routeName] = ['class' => $controllerClass, 'method' => $controllerMethod];
}
unset($controller);
unset($route);
$cache->set('route_class_map', $controllers);
}
public function isOptional()
{
return false;
}
}
And in my RouteHelper, the implementation reading this looks like this
$cache = new PhpFilesCache('route_class_map', 0, $this->cacheDirectory);
$controllers = $cache->get('route_class_map');
if (!isset($controllers[$routeName])) {
throw new CacheException('No entry for route ' . $routeName . ' forund in RouteClassMap cache, please warmup first.');
}
if (null !== $securityAnnotation = $this->annotationReader->getMethodAnnotation((new \ReflectionClass($controllers[$routeName]['class']))->getMethod($controllers[$routeName]['method']), Security::class))
{
return $this->securityExpressionHelper->evaluate($securityAnnotation->getExpression(), ['myParameter' => $myParameter]);
}
This should be much faster than getting the routeCollection and resolve the service_id:method notated _controller-properties against the container on every request.

Symfony2 and ParamConverter(s)

Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values

Resources