I'd like to create a custom Exception on Symfony3 that returns a JSON response to be able to handle it in JavaScript afterwards.
Does someone know if it's possible and how to do it ?
Create a new exception handler class, like this:
namespace AppBundle\Subscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
class ExceptionSubscriber implements EventSubscriberInterface
{
/* ... */
public static function getSubscribedEvents()
{
return [ KernelEvents::EXCEPTION => 'onKernelException' ];
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$customResponse = new JsonResponse(['error' => 'My custom error message']);
$event->setResponse($customResponse);
}
}
Don't forget to register the new service in app/config/services.yml:
app.exception_subscriber:
class: AppBundle\Subscriber\ExceptionSubscriber
tags:
- { name: kernel.event_subscriber }
Related
The application that I am building is not going to work in a traditional way. All the routes ar going to be stored in the database. And based on the route provided I need to get the correct controller and action to be executed.
As I understand this can be achieved using the "kernel.controller" event listener: https://symfony.com/doc/current/reference/events.html#kernel-controller
I am trying to use the docs provided, but the example here does not exacly show how to set up a new callable controller to be passed. And I have a problem here, because I dont know how to inject the service container to my newly called controller.
At first the setup:
services.yaml
parameters:
db_i18n.entity: App\Entity\Translation
developer: '%env(DEVELOPER)%'
category_directory: '%kernel.project_dir%/public/uploads/category'
temp_directory: '%kernel.project_dir%/public/uploads/temp'
product_directory: '%kernel.project_dir%/public/uploads/product'
app.supported_locales: 'lt|en|ru'
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
App\Translation\DbLoader:
tags:
- { name: translation.loader, alias: db }
App\Extension\TwigExtension:
arguments:
- '#service_container'
tags:
- { name: twig.extension }
App\EventListener\RequestListener:
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onControllerRequest }
The listener:
RequestListener.php
<?php
namespace App\EventListener;
use App\Controller\Shop\HomepageController;
use App\Entity\SeoUrl;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
class RequestListener
{
public ManagerRegistry $doctrine;
public RequestStack $requestStack;
public function __construct(ManagerRegistry $doctrine, RequestStack $requestStack)
{
$this->doctrine = $doctrine;
$this->requestStack = $requestStack;
}
/**
* #throws Exception
*/
public function onControllerRequest(ControllerEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
if(str_contains($this->requestStack->getMainRequest()->getPathInfo(), '/admin')) {
return;
}
$em = $this->doctrine->getManager();
$pathInfo = $this->requestStack->getMainRequest()->getPathInfo();
;
$route = $em->getRepository(SeoUrl::class)->findOneBy(['keyword' => $pathInfo]);
if($route instanceof SeoUrl) {
switch ($route->getController()) {
case 'homepage':
$controller = new HomepageController();
$event->setController([$controller, $route->getAction()]);
break;
default:
break;
}
} else {
throw new Exception('Route not found');
}
}
}
So this is the most basic example. I get the route from the database, if it a "homepage" route, I create the new HomepageController and set the action. However I am missing the container interface that I dont know how to inject. I get this error:
Call to a member function has() on null
on line: vendor\symfony\framework-bundle\Controller\AbstractController.php:216
which is:
/**
* Returns a rendered view.
*/
protected function renderView(string $view, array $parameters = []): string
{
if (!$this->container->has('twig')) { // here
throw new \LogicException('You cannot use the "renderView" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".');
}
return $this->container->get('twig')->render($view, $parameters);
}
The controller is as basic as it gets:
HomepageController.php
<?php
namespace App\Controller\Shop;
use App\Repository\CategoryRepository;
use App\Repository\Shop\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HomepageController extends AbstractController
{
#[Route('/', name: 'index', methods: ['GET'])]
public function index(): Response
{
return $this->render('shop/index.html.twig', [
]);
}
}
So basically the container is not set. If I dump the $event->getController() I get this:
RequestListener.php on line 58:
array:2 [▼
0 => App\Controller\Shop\HomepageController {#417 ▼
#container: null
}
1 => "index"
]
I need to set the container by doing $controller->setContainer(), but what do I pass?
Do not inject the container, controllers are services too and manually instanciating them is preventing you from using constructor dependency injection. Use a service locator which contains only the controllers:
Declared in config/services.yaml:
# config/services.yaml
services:
App\EventListener\RequestListener:
arguments:
$serviceLocator: !tagged_locator { tag: 'controller.service_arguments' }
Then in the event listener, add the service locator argument and fetch the fully configured controllers from it:
# ...
use App\Controller\Shop\HomepageController;
use Symfony\Component\DependencyInjection\ServiceLocator;
class RequestListener
{
# ...
private ServiceLocator $serviceLocator;
public function __construct(
# ...
ServiceLocator $serviceLocator
) {
# ...
$this->serviceLocator = $serviceLocator;
}
public function onControllerRequest(ControllerEvent $event)
{
# ...
if($route instanceof SeoUrl) {
switch ($route->getController()) {
case 'homepage':
$controller = $this->serviceLocator->get(HomepageController::class);
# ...
break;
default:
break;
}
}
# ...
}
}
If you dump any controller you will see that the container is set. Same will go for additionnal service that you autowire from the constructor.
On my Symnfony3 project I noticed that during registration some events are generated where I can override the response. eg. Instead of rendering the default twig template and redirect to just return a JsonResponse with a successMessage.
Therefore I did the following Event Subscriber:
namespace AppBundle\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use AppBundle\Constants\AjaxJsonResponseConstants;
use Symfony\Component\HttpFoundation\JsonResponse;
use FOS\UserBundle\Event\FilterUserResponseEvent;
class UserRegistrationResponseChanger implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
$subscribedEvents=[
// FOSUserEvents::REGISTRATION_INITIALIZE=>[],
FOSUserEvents::REGISTRATION_COMPLETED=>[],
FOSUserEvents::REGISTRATION_SUCCESS=>["setJsonResponseOnSuccess",-1],
FOSUserEvents::REGISTRATION_FAILURE=>["setJsonResponseOnFailure",-1],
// FOSUserEvents::REGISTRATION_CONFIRM=>[],
// FOSUserEvents::REGISTRATION_CONFIRMED=>[]
];
}
public function setJsonResponseOnSuccess(FormEvent $formEvent)
{
$response=['status'=>AjaxJsonResponseConstants::AJAX_ACTION_SUCCESS,'message'=>"User Sucessfully Registered please check your mail."];
$response=new JsonResponse($response);
$formEvent->setResponse($response);
return $response;
}
public function setJsonResponseOnFailure(FormEvent $formEvent)
{
$response=['status'=>AjaxJsonResponseConstants::AJAX_ACTION_FAIL,'message'=>"You cannot register please try again later"];
$response=new JsonResponse($response);
$formEvent->setResponse($response);
return $response;
}
}
Also on my services.yml I have put the following:
app.user_register.subscriber:
class: AppBundle\EventSubscriber\UserRegistrationResponseChanger
tags:
- { name: app.user_register.subscriber }
And the command
In order to override on how the response will get returned but somehow it fails to do so and redirects to the default page. What I try to acheive it to perform the registration via ajax call instead of rendering the registration page and redirecting.
You should prioritize the REGISTRATION_SUCCESS event when you have registration confirmation (default behaviour in FOSUserBundle), see http://symfony.com/doc/master/bundles/FOSUserBundle/controller_events.html#registration-success-listener-with-enabled-confirmation-at-the-same-time
The service definition needs to be like this:
#app/config/services.yml
app.security_registration_success:
class: Path\To\Your\EventListener\RegistrationSuccessListener
tags:
- { name: kernel.event_subscriber }
An example of a registration success listener:
<?php
declare(strict_types=1);
namespace Path\To\Your\EventListener;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\FOSUserEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
class RegistrationSuccessListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [FOSUserEvents::REGISTRATION_SUCCESS => [['onRegistrationSuccess', -10]]];
}
public function onRegistrationSuccess(FormEvent $event): void
{
$event->setResponse(new JsonResponse());
}
}
You should do this steps:
First of all you should use kernel.event_subscriber instead of app.user_register.subscriber when you define the event subscriber therfore your subscriber will be defined like that:
app.user_register.subscriber:
class: AppBundle\EventSubscriber\UserRegistrationResponseChanger
tags:
- { name: kernel.event_subscriber }
To the services.yml.
Furthermore the getSubscribedEvents must return the array of the listeners. Also the FOSUserEvents::REGISTRATION_COMPLETED MUST Have a listener even if it isdoes not have an implementation, if you do not want a listener just comment the like.
In the end your listener shuld be implemented like that:
namespace AppBundle\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use AppBundle\Constants\AjaxJsonResponseConstants;
use Symfony\Component\HttpFoundation\JsonResponse;
use FOS\UserBundle\Event\FilterUserResponseEvent;
class UserRegistrationResponseChanger implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
$subscribedEvents=[
// FOSUserEvents::REGISTRATION_INITIALIZE=>[],
// FOSUserEvents::REGISTRATION_COMPLETED=>[],
FOSUserEvents::REGISTRATION_SUCCESS=>["setJsonResponseOnSuccess",-1],
FOSUserEvents::REGISTRATION_FAILURE=>["setJsonResponseOnFailure",-1],
// FOSUserEvents::REGISTRATION_CONFIRM=>[],
// FOSUserEvents::REGISTRATION_CONFIRMED=>[]
];
return $subscribedEvents;
}
public function setJsonResponseOnSuccess(FormEvent $formEvent)
{
$response=['status'=>AjaxJsonResponseConstants::AJAX_ACTION_SUCCESS,'message'=>"User Sucessfully Registered please check your mail."];
$response=new JsonResponse($response);
$formEvent->setResponse($response);
return $response;
}
public function setJsonResponseOnFailure(FormEvent $formEvent)
{
$response=['status'=>AjaxJsonResponseConstants::AJAX_ACTION_FAIL,'message'=>"You cannot register please try again later"];
$response=new JsonResponse($response);
$formEvent->setResponse($response);
return $response;
}
}
This is the first time ever I am working with creating custom event dispatcher and subscriber so I am trying to wrap my head around it and I cant seem to find out why my custom event is not being dispatched.
I am following the documentation and in my case I need to dispatch an event as soon as someone registers on the site.
so inside my registerAction() I am trying to dispatch an event like this
$dispatcher = new EventDispatcher();
$event = new RegistrationEvent($user);
$dispatcher->dispatch(RegistrationEvent::NAME, $event);
This is my RegistrationEvent class
namespace AppBundle\Event;
use AppBundle\Entity\User;
use Symfony\Component\EventDispatcher\Event;
class RegistrationEvent extends Event
{
const NAME = 'registration.complete';
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function getUser(){
return $this->user;
}
}
This is my RegistrationSubscriber class
namespace AppBundle\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RegistrationSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
KernelEvents::RESPONSE => array(
array('onKernelResponsePre', 10),
array('onKernelResponsePost', -10),
),
RegistrationEvent::NAME => 'onRegistration'
);
}
public function onKernelResponsePre(FilterResponseEvent $event)
{
// ...
}
public function onKernelResponsePost(FilterResponseEvent $event)
{
// ...
}
public function onRegistration(RegistrationEvent $event){
var_dump($event);
die;
}
}
After doing this, I was hoping that the registration process would stop at the function onRegistration but that did not happen, I then looked at the Events tab of the profiler and I do not see my Event listed their either.
What am I missing here? A push in right direction will really be appreciated.
Update:
I thought i need to register a service for the custom event so I added the following code inside services.yml
app.successfull_registration_subscriber:
class: AppBundle\Event\RegistrationSubscriber
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: kernel.event_subscriber}
Inside the Event tab of profiler I do see my custom event being listed but it still does not dispatch.
By creating your own EventDispatcher instance you dispatch an event that can never be listened to by other listeners (they are not attached to this dispatcher instance). You need to use the event_dispatcher service to notify all listeners you have tagged with the kernel.event_listener and kernel.event_subscriber tags:
// ...
class RegistrationController extends Controller
{
public function registerAction()
{
// ...
$this->get('event_dispatcher')->dispatch(RegistrationEvent::NAME, new RegistrationEvent($user););
}
}
Duplicate of dispatcher doesn't dispatch my event symfony
With auto-wiring, it is now better to inject the EventDispatcherInterface
<?php
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
//...
class DefaultController extends Controller
{
public function display(Request $request, EventDispatcherInterface $dispatcher)
{
//Define your event
$event = new YourEvent($request);
$dispatcher->dispatch(YourEvent::EVENT_TO_DISPATCH, $event);
}
}
I was using Authentication success handler to populate some values on session on every success login. I wanted some database operation to be done so i pass #doctrine.dbal.default_connection from my config file. Here is my config file where i override success_handler function.
services:
security.authentication.success_handler:
class: XYZ\UserBundle\Handler\AuthenticationSuccessHandler
arguments: ["#security.http_utils", {}, #doctrine.dbal.default_connection]
tags:
- { name: 'monolog.logger', channel: 'security' }
In AuthenticationSuccessHandler.php my code is like this...
namespace Sourcen\UserBundle\Handler;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\HttpUtils;
use Doctrine\DBAL\Connection;
class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler {
private $connection;
public function __construct( HttpUtils $httpUtils, array $options, Connection $dbalConnection ) {
$this->connection = $dbalConnection;
parent::__construct( $httpUtils, $options );
}
public function onAuthenticationSuccess( Request $request, TokenInterface $token ) {
$response = parent::onAuthenticationSuccess( $request, $token );
// DB CODE GOES
return $response;
}
}
This is working when i execute some controller URL directly. But when i execute my app home url like "www.xyz.com/web" it throws following error...
Catchable fatal error: Argument 3 passed to XYZ\UserBundle\Handler\AuthenticationSuccessHandler::__construct() must be an instance of Doctrine\DBAL\Connection, none given, called in /opt/lampp/xyz/app/cache/prod/appProdProjectContainer.php on line 1006 and defined in /opt/lampp/xyz/src/Sourcen/UserBundle/Handler/AuthenticationSuccessHandler.php on line 18
Any idea how it can be solved ?
You don't need to extends the DefaultAuthenticationSuccessHandlerclass.
Try to define your service class like:
namespace XYZ\UserBundle\Handler;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Doctrine\DBAL\Connection;
class AuthenticationSuccessHandler {
private $connection;
public function __construct( Connection $dbalConnection ) {
$this->connection = $dbalConnection;
}
public function onAuthenticationSuccess( InteractiveLoginEvent $event ) {
$user = $event->getAuthenticationToken()->getUser();
// DB CODE GOES
return $response;
}
}
and configure your service tagged to the event listener component security.interactive_login
services:
security.authentication.success_handler:
class: XYZ\UserBundle\Handler\AuthenticationSuccessHandler
arguments: [#doctrine.dbal.default_connection]
tags:
- { name: 'kernel.event_listener', event: 'security.interactive_login'. method:'onAuthenticationSuccess' }
PS: Why don't you use the doctrine.orm.entity_managerinstead of doctrine.dbal.default_connection? (In my sf i haven't this service dumped in the php app/console container:debug command)
namespace etc...
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpFoundation\Response;
class MyExceptionController extends ExceptionController
{
public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null, $format = 'html')
{
}
}
Doing nothing inside the controller returns a "Uncaught exception 'Symfony\Component\Routing\Exception\ResourceNotFoundException' in..." error. Not sure if that's right, or if that's another problem. I'd expect it to just do the usual action.
I just need to do it so it shows a specified route exactly as it would if I went to domain.com/page.
I've tried this:
$httpKernel = $this->container->get('kernel');
$response = $httpKernel->forward('AcmeMyBundle:Default:pageAction');
$this->setResponse(new Response($response));
...but get this error:
Call to a member function get() on a non-object in...
Your code looks similar to something I did yesterday. I wanted to get all NotFoundHttpException Exception and try to forward them to a default controller. I achieved this with an exception listener like this:
<?php
namespace Acme\MyBundle\Listener;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
class NotFoundHttpExceptionListener
{
protected $container;
public function setContainer($container)
{
$this->container = $container;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if ($exception instanceof NotFoundHttpException) {
$httpKernel = $this->container->get('http_kernel');
$response = $httpKernel->forward(
'AcmeMyBundle:Controller:action',
array(
'uri' => $_SERVER['REQUEST_URI'],
)
);
$response->headers->set('X-Status-Code', '200');
$event->setResponse($response);
$event->stopPropagation();
}
}
}
Note that X-Status-Code is necessary if you want to return another status code than 404 because the handleException method in HttpKernel will use this to set the final status code and removes it from the header section.
My services.yml looks something like this:
notfoundhttp.exception.listener:
class: Acme\MyBundle\Listener\NotFoundHttpExceptionListener
calls:
- [ setContainer, [#service_container] ]
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }