Inject a flash on specific exception - symfony

I'm extending a custom exception for all my handled "application errors"
abstract class AbstractApplicationException extends \Exception
{
public function __construct(array $context = array())
{
$this->context = $context;
parent::__construct('exceptions.'.lcfirst(self::classname()));
}
}
And I use messages.xx.yml to describe the error to the user
exceptions:
incompatibleSettings: Vos réglages ne sont pas compatibles
I'd like to automatically inject a flash with the translated message on exception (of kind AbstractApplicationException) so I don't have to do it on all my controllers
public myControllerAction()
try {
$someService->someFunction();
} catch (AbstractApplicationException $e) {
$flashBag->add('error',
$this->get('translator')->trans(
$e->getMessage(), $e->getContext()
)
);
}
$this->render('related_template.html.twig');
}
I know how to redirect the user with a listener, but I actually want the user to land on the specific action reponse, only with a flash injected.

You can create an Exception listener:
# app/config/services.yml
services:
kernel.listener.your_listener_name:
class: AppBundle\EventListener\AcmeExceptionListener
arguments: [#router, #session]
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
Then set flash message and redirect user to the action you want, which will render it:
class AcmeExceptionListener
{
/**
* #var UrlGeneratorInterface
*/
private $router;
/**
* #var SessionInterface
*/
private $session;
public function __construct(UrlGeneratorInterface $router, SessionInterface $session)
{
$this->router = $router;
$this->session = $session;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if ($exception instanceof YourExceptionType) {
$this->session->getFlashBag()->add('error', $exception->getMessage());
$event->setResponse(new RedirectResponse($this->router->generate('your_route')));
}
}
}
Be careful with this though: if during exception handling you again throw YourExceptionType, you will end up having infinite redirect loop.

I finally used a trait to be able to continue the execution of my controllers with the flash injected.
/**
* Class ControllerTrait
* #requirement The using class must be containerAware
*/
trait ControllerTrait {
public function injectExceptionFlash(AbstractApplicationException $e) {
if (!isset($this->container)) {
throw new \Exception(sprintf('You must containerAware to use %s',
__TRAIT__
));
}
$flashBag = $this->container->get('session')->getFlashBag();
$flashBag->add('error',
$this->container->get('translator')->trans(
$e->getMessage(), $e->getContext()
)
);
}
public function tryFlash($lambda) {
try {
$lambda();
} catch (AbstractApplicationException $e) {
$this->injectExceptionFlash($e);
}
}
}
Here is how I use it from my controllers
use ControllerTrait;
public myControllerAction()
$this->tryFlash(function () (use $someParam) {
$someService->someFunction($someParam);
});
$this->render('related_template.html.twig');
}
tryFlash() is a shortcut using a lambda function to do the try/catch/flash jobs
Don't hesitate to tell me if there is a bad practice somewhere

Related

Symfony route access check

I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:
/**
* #Route("/products/{id}/edit")
*/
public function editAction(Request $request, Product $product)
{
// security
$user = $this->getUser();
if ($user != $product->getUser()) {
throw $this->createAccessDeniedException();
}
// ...
}
How can I avoid making the same check on every action (bonus points if using annotations and expressions)?
I am already using security.yml with access_control to deny access based on roles.
You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.
You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!
Here is an example you can use. You may have to change a few items but this is basically it.
To read more visit: https://symfony.com/doc/current/security/voters.html
<?php
namespace AppBundle\Security;
use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class ProductVoter extends Voter
{
const EDIT = 'EDIT_USER_PRODUCT';
protected function supports($attribute, $subject)
{
if($attribute !== self::EDIT) {
return false;
}
if(!$subject instanceof Product) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** #var Product $product */
$product= $subject;
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
return $this->belongsToUser($product, $user);
}
private function belongsToUser(Product $product, User $user)
{
return $user->getId() === $product->getUser()->getId();
}
}
You could try with a listener:
Check the action name,for example, if it is "edit_product", them continue.
Get the current logged User.
Get the user of the product entity.
Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.
services.yml
app.user.listener:
class: AppBundle\EventListener\ValidateUserListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
Edit Action:
Added name "edit_product" to the action.
/**
*
* #Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
...
src\AppBundle\EventListener\ValidateUserListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ValidateUserListener
{
private $container;
private $entityManager;
public function __construct($container, $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
$currentRoute = $event->getRequest()->attributes->get('_route');
if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
{
$array_user = $this->getCurrentUser();
if($array_user['is_auth'])
{
$current_user = $array_user['current_user'];
$product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);
$product_user = $product->getUsername();
if ($current_user !==$product_user)
{
throw $this->createAccessDeniedException();
}
}
}
}
private function getCurrentUser()
{
//Get the current logged User
$user = $this->container->get('security.token_storage')->getToken()->getUser();
if(null!=$user)
{
//If user is authenticated
$isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
return array('is_auth'=>$isauth, 'current_user'=>$user);
}
return array('is_auth'=>false, 'current_user'=>$user);
}
}
Tested in Symfony 3.3

Access to logger within an ExceptionListener in Symfony 4

I'm trying to log some informations with the logger service within an ExceptionListener class but I don't understand how to access / create a logger object...
Here is my piece of code :
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
...
$exception = $event->getException();
if ($exception instanceof HttpExceptionInterface) {
// HTTP Exception (400, 401, 404, ...)
$response = new JsonResponse(...)
}
...
$event->setResponse($response);
}
}
The listener works perfectly but in this case nothing is logged by default into the dev.log file (of course logging is enabled and functional).
I tried to had an LoggerInterface parameter to the onKernelException function (autowiring ?) but without success.
Should I had some additional configuration in the service.yaml file ?
App\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
Autowiring being by default on SF4, you should be able to inject the LoggerInterface into your listener without further configuration, like so (then call $this->logger when desired) :
use Psr\Log\LoggerInterface;
class ExceptionListener
{
protected $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
...
$exception = $event->getException();
if ($exception instanceof HttpExceptionInterface) {
// HTTP Exception (400, 401, 404, ...)
$response = new JsonResponse(...)
}
...
$event->setResponse($response);
}
}

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();
}
}

How to get Request object inside a Twig Extension in Symfony?

How can one access the Request object inside Twig Extension?
namespace Acme\Bundle\Twig;
use Twig_SimpleFunction;
class MyClass extends \Twig_Extension
{
public function getFunctions()
{
return array(
new Twig_SimpleFunction('xyz', function($param) {
/// here
$request = $this->getRequestObject();
})
);
}
public function getName() {
return "xyz";
}
}
As requested in the comments, here's the prefered way of injecting a request into any service. It works with Symfony >= 2.4.
Injecting the request and putting our service in the request scope is no longer recommended. We should use the request stack instead.
namespace AppBundle\Twig;
use Symfony\Component\HttpFoundation\RequestStack;
class MyClass extends \Twig_Extension
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getFunctions()
{
$requestStack = $this->requestStack;
return array(
new \Twig_SimpleFunction('xyz', function($param) use ($requestStack) {
$request = $requestStack->getCurrentRequest();
})
);
}
public function getName()
{
return "xyz";
}
}
app/config/services.yml
app.twig_extension:
class: AppBundle\Twig\MyExtension
arguments:
- '#request_stack'
tags:
- { name: twig.extension }
Docs:
the request stack API
the request stack announcement
Register your extension as a service and give it the container service:
# services.yml
services:
sybio.twig_extension:
class: %sybio.twig_extension.class%
arguments:
- #service_container
tags:
- { name: twig.extension, priority: 255 }
Then retrieve the container by your (twig extension) class constructor and then the request:
<?php
// Your class file:
// ...
class MyClass extends \Twig_Extension
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var Request
*/
protected $request;
/**
* Constructor
*
* #param ContainerInterface $container
*/
public function __construct($container)
{
$this->container = $container;
if ($this->container->isScopeActive('request')) {
$this->request = $this->container->get('request');
}
}
// ...
Note that testing the scope is usefull because there is no request when running console command, it avoids warnings.
That's it, you are able to use the request !
I would suggest setting 'needs_environment' => true for your Twig_SimpleFunction, which then will add \Twig_Environment as first argument of your function. Then in your function you can find the request like this:
$request = $twig->getGlobals()['app']->getRequest();
So the whole function will look like this:
...
public function getFunctions() {
return [
new \Twig_SimpleFunction('xyz', function(\Twig_Environment $env) {
$request = $twig->getGlobals()['app']->getRequest();
}, [
'needs_environment' => true,
]),
];
}
...

How do I reduce the severity of NotFoundHttpException?

I want to be alerted when bad things happen in my Symfony2 app. Right now I just look for ERROR in the logs. Unfortunately, "HTTP 404 - file not found" (NotFoundHttpException) gets logged as an error, as does "HTTP 403 - forbidden" (AccessDeniedHttpException).
This doesn't warrant an error; at the most these should be warnings. How can I make these log at a less severe level?
Example error:
[2012-07-02 16:58:21] request.ERROR: Symfony\Component\HttpKernel\Exception\NotFoundHttpException: No route found for "GET /foo" (uncaught exception) at /home/user/Symfony2_v2.0.12/vendor/symfony/src/Symfony/Bundle/FrameworkBundle/EventListener/RouterListener.php line 83 [] []
I found something that works. The Symfony2 internals doc on the kernel.exeption event mention that a response can be set on the event, and the GetResponseForExceptionEvent docs say
The propagation of this event is stopped as soon as a
response is set.
I cobbled together a listener that appears to do just what I want:
<?php
namespace Acme\DemoBundle\Listener;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
class ExceptionLoggingListener {
private $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event) {
if(!$event) {
$this->logger->err("Unknown kernel.exception in ".__CLASS__);
return;
}
$notFoundException = '\Symfony\Component\HttpKernel\Exception\NotFoundHttpException';
$e = $event->getException();
$type = get_class($e);
if ($e instanceof $notFoundException) {
$this->logger->info($e->getMessage());
$response = new Response(Response::$statusTexts[404], 404);
$event->setResponse($response);
return;
}
$accessDeniedException = '\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException';
if ($e instanceof $accessDeniedException) {
$this->logger->info($e->getMessage());
$response = new Response(Response::$statusTexts[403], 403);
$event->setResponse($response);
return;
}
$this->logger->err("kernel.exception of type $type. Message: '".$e->getMessage()."'\nFile: ".$e->getFile().", line ".$e->getLine()."\nTrace: ".$e->getTraceAsString());
}
}
Just add excluded_404s to your configuration:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_404s:
- ^/
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
See http://symfony.com/doc/current/logging/monolog_regex_based_excludes.html for a reference
Here's a way with less code :)
1. Extend Symfonys ExceptionListner class and override the logging method:
<?php
use Symfony\Component\HttpKernel\EventListener\ExceptionListener as BaseListener;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ExceptionListener extends BaseListener
{
/**
* Logs an exception.
*
* #param \Exception $exception The original \Exception instance
* #param string $message The error message to log
* #param Boolean $original False when the handling of the exception thrown another exception
*/
protected function logException(\Exception $exception, $message, $original = true)
{
$isCritical = !$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500;
if (null !== $this->logger) {
if ($isCritical) {
$this->logger->critical($message);
} else {
if ($exception instanceof NotFoundHttpException) {
$this->logger->info($message);
} else {
$this->logger->error($message);
}
}
} elseif (!$original || $isCritical) {
error_log($message);
}
}
}
2. Configure the twig.exception_listener.class parameter:
parameters:
twig.exception_listener.class: "MyBundle\EventListener\ExceptionListener"
You can also use error level activity strategy (actually Symfony's in-built 404 errors excluding is done using this, so I guess this is a proper way to do it).
config.yml
monolog:
handlers:
main:
type: fingers_crossed
handler: loggly
activation_strategy: 'mybundle.monolog.fingers_crossed.activation_strategy'
loggly:
type: loggly
token: %loggly_token%
level: error
tag: %loggly_tag%
services.yml (note that action level is set here, not in config.yml)
services:
mybundle.monolog.fingers_crossed.activation_strategy:
class: MyBundle\Handler\FingersCrossed\ErrorLevelActivationStrategy
arguments:
- '#request_stack'
- 'error'
ErrorLevelActivationStrategy.php
<?php
namespace MyBundle\Handler\FingersCrossed;
use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy as BaseErrorLevelActivationStrategy;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Activation strategy that ignores client errors (4xx)
*/
class ErrorLevelActivationStrategy extends BaseErrorLevelActivationStrategy
{
protected $requestStack;
public function __construct(RequestStack $requestStack, $actionLevel)
{
parent::__construct($actionLevel);
$this->requestStack = $requestStack;
}
/**
* {#inheritdoc}
*/
public function isHandlerActivated(array $record)
{
$isActivated = parent::isHandlerActivated($record);
if (
$isActivated
&& isset($record['context']['exception'])
&& $record['context']['exception'] instanceof HttpException
&& $record['context']['exception']->getStatusCode() >= 400
&& $record['context']['exception']->getStatusCode() <= 499
&& ($request = $this->requestStack->getMasterRequest())
) {
$isActivated = false;
}
return $isActivated;
}
}
https://gist.github.com/sobstel/d791d0347ee1f4e47b6e
Symfony 5 solution based on Tarjei's answer:
Create a new ErrorListener that overrides the Symfony ErrorListener.
App\EventListener\ErrorListener.php
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\HttpKernel\EventListener\ErrorListener as BaseListener;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ErrorListener extends BaseListener
{
/**
* Logs an exception.
*
* #param \Exception $exception The original \Exception instance
* #param string $message The error message to log
*/
protected function logException(\Throwable $exception, string $message): void
{
$isCritical = !$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500;
if (null !== $this->logger) {
if ($isCritical) {
$this->logger->critical($message);
} else {
if ($exception instanceof NotFoundHttpException) {
$this->logger->info($message);
} else {
$this->logger->error($message);
}
}
}
}
}
services.yaml
services:
App\EventListener\ErrorListener: '#exception_listener'
exception_listener:
autowire: false
class: App\EventListener\ErrorListener
arguments:
$controller: '%kernel.error_controller%'
$logger: '#logger'
$debug: '%kernel.debug%'
Preferred this in Symfony 3.3 instead of excluded_404s parameter.
app/config/services.yml
exception_listener:
class: AppBundle\Listener\ExceptionListener
arguments: ["#logger", "#templating"]
tags:
- { name: kernel.event_listener, event: kernel.exception}
src/AppBundle/Listener/ExceptionListener.php
<?php
namespace AppBundle\Listener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Bridge\Monolog\Logger;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Templating\EngineInterface;
class ExceptionListener
{
private $logger;
private $templateEngine;
public function __construct(Logger $logger, EngineInterface $templateEngine)
{
$this->logger = $logger;
$this->templateEngine = $templateEngine;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if ($exception instanceof NotFoundHttpException)
{
$this->logger->info($exception->getMessage());
// default twig error app/config/Resources/TwigBundle/views/Exception/error.html.twig
$response = $this->templateEngine->render('TwigBundle:Exception:error.html.twig');
$event->setResponse(new Response($response));
}
}
}

Resources