Symfony: Best place to catch exception (CQRS/DDD) - php

I've a personal application. I use design pattern CQRS/DDD for a API.
Schema:
User --> Controller (dispatch command) --> Command handler --> some services...
In my Rest API controller
$this->dispatch($cmd);
If a throw a exception in services or specification classes for example, ok, I've a listener to catch exception and create JSON response error.
But if I want to develop an interface module with TWIG, I think I will not use my listener because I don't want a JSON response.
Should I used try/catch in my controller of my new interface module ?
SomeController extends AbstractController
{
public function getObject($id)
{
try {
$this->dispatch($cmd);
catch(SomeException $ex) {
$this->render(....)
}
}
}
Where is the best place to catch exception for TWIG ?
Thanks.
Edit:
#Cid
if (some conditions && $form->handleRequest($request)->isValid()) --> My handler don't return bool or values.
Imagine this code. Imagine I want share a service between an API and web view app.
class ApiController
{
public function register()
{
$this->dispatch($cmd);
}
}
class WebController
{
public function register()
{
$this->dispatch($cmd);
}
}
class SomeHandler implements CommandHandlerInterface
{
/** #required */
public RegisterService $service;
public function __invoke(SomeCommand $command)
{
$this->service->register($command->getEmail())
}
}
class RegisterService
{
public function register(string $email)
{
// Exception here
}
}

So, I think the best place to handle Exception is EventSubscriber, see here: https://symfony.com/doc/current/reference/events.html#kernel-exception
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
public function onKernelException(ExceptionEvent $event)
{
$exception = $event->getThrowable();
$response = new Response();
// setup the Response object based on the caught exception
$event->setResponse($response);
// you can alternatively set a new Exception
// $exception = new \Exception('Some special exception');
// $event->setThrowable($exception);
}

Related

Load Symfony subscriber from package

I am currently developing a library that will be used to intercept incoming exceptions, log them, and pass the to a separate micro-service for handling logs. So inside of the composer lib I am developing I need to have a event subscriber that will listen for exceptions and if the class that throws the exceptions extend our interface log it. Currently I have the interface
namespace LogLibrary;
interface ExceptionLoggerInterface
{
}
And the event subscriber
final class ExceptionSubscriber implements EventSubscriberInterface
{
public function __construct()
{
$this->logger = new LoggerService();
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => ['logException', 0],
];
}
public function logException(ResponseEvent|ExceptionEvent $event): void
{
dd($event);
$exception = $event->getThrowable();
if ($exception instanceof ExceptionLoggerInterface) {
$this->logger->logError($exception);
}
}
}
And finally on any class from where I have to catch exception I have this
class UpdatePinCodeController implements ExceptionLoggerInterface
{...}
The problem is to actually make the subscriber work. I have the default doctribe setup, autowire is true, I followed the docs extensively. Any help on how to register the subscriber without actually writing anything in the services.yaml file?

beforeFilter function not redirecting in Symfony2

I have implemented following code to run a code on before any action of any controller. However, the beforeFilter() function not redirecting to the route I have specified. Instead it takes the user to the location where the user clicked.
//My Listener
namespace Edu\AccountBundle\EventListener;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class BeforeControllerListener
{
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
{
//not a controller do nothing
return;
}
$controllerObject = $controller[0];
if (is_object($controllerObject) && method_exists($controllerObject, "beforeFilter"))
//Set a predefined function to execute Before any controller Executes its any method
{
$controllerObject->beforeFilter();
}
}
}
//I have registered it already
//My Controller
class LedgerController extends Controller
{
public function beforeFilter()
{
$commonFunction = new CommonFunctions();
$dm = $this->getDocumentManager();
if ($commonFunction->checkFinancialYear($dm) == 0 ) {
$this->get('session')->getFlashBag()->add('error', 'Sorry');
return $this->redirect($this->generateUrl('financialyear'));//Here it is not redirecting
}
}
}
public function indexAction() {}
Please help, What is missing in it.
Thanks Advance
I would suggest you follow the Symfony suggestions for setting up before and after filters, where you perform your functionality within the filter itself, rather than trying to create a beforeFilter() function in your controller that is executed. It will allow you to achieve what you want - the function being called before every controller action - as well as not having to muddy up your controller(s) with additional code. In your case, you would also want to inject the Symfony session to the filter:
# app/config/services.yml
services:
app.before_controller_listener:
class: AppBundle\EventListener\BeforeControllerListener
arguments: ['#session', '#router', '#doctrine_mongodb.odm.document_manager']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Then you'll create your before listener, which will need the Symony session and routing services, as well as the MongoDB document manager (making that assumption based on your profile).
// src/AppBundle/EventListener/BeforeControllerListener.php
namespace AppBundle\EventListener;
use Doctrine\ODM\MongoDB\DocumentManager;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use AppBundle\Controller\LedgerController;
use AppBundle\Path\To\Your\CommonFunctions;
class BeforeControllerListener
{
private $session;
private $router;
private $documentManager;
private $commonFunctions;
public function __construct(Session $session, Router $router, DocumentManager $dm)
{
$this->session = $session;
$this->router = $router;
$this->dm = $dm;
$this->commonFunctions = new CommonFunctions();
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof LedgerController) {
if ($this->commonFunctions->checkFinancialYear($this->dm) !== 0 ) {
return;
}
$this->session->getFlashBag()->add('error', 'Sorry');
$redirectUrl= $this->router->generate('financialyear');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
If you are in fact using the Symfony CMF then the Router might actually be ChainRouter and your use statement for the router would change to use Symfony\Cmf\Component\Routing\ChainRouter;
There are a few additional things here you might want to reconsider - for instance, if the CommonFunctions class needs DocumentManager, you might just want to make your CommonFunctions class a service that injects the DocumentManager automatically. Then in this service you would only have to inject your common functions service instead of the document manager.
Either way what is happening here is that we are checking that we are in the LedgerController, then checking whether or not we want to redirect, and if so we overwrite the entire Controller via a callback. This sets the redirect response to your route and performs the redirect.
If you want this check on every single controller you could simply eliminate the check for LedgerController.
.
$this->redirect() controller function simply creates an instance of RedirectResponse. As with any other response, it needs to be either returned from a controller, or set on an event. Your method is not a controller, therefore you have to set the response on the event.
However, you cannot really set a response on the FilterControllerEvent as it is meant to either update the controller, or change it completely (setController). You can do it with other events, like the kernel.request. However, you won't have access to the controller there.
You might try set a callback with setController which would call your beforeFilter(). However, you wouldn't have access to controller arguments, so you won't really be able to call the original controller if beforeFilter didn't return a response.
Finally you might try to throw an exception and handle it with an exception listener.
I don't see why making things this complex if you can simply call your method in the controller:
public function myAction()
{
if ($response = $this->beforeFilter()) {
return $response;
}
// ....
}
public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
$response = new Response();
// Matched route
$_route = $request->attributes->get('_route');
// Matched controller
$_controller = $request->attributes->get('_controller');
$params = array(); //Your params
$route = $event->getRequest()->get('_route');
$redirectUrl = $url = $this->container->get('router')->generate($route,$params);
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
Cheers !!

Laravel Exceptions Handler not work

Environment: Laravel 5.1, PHP 5.6.10
I tried to implement App\Exceptions\Handler::render() to response error message in JSON format.
The app/Exceptions/Handler.php as follows:
// ignore..
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException) {
$e = new NotFoundHttpException($e->getMessage(), $e);
} elseif ($e instanceof AbstractException) {
return response()->apiJsonError(
$e->getMessage(),
$e->getErrors(),
$e->statusCode());
}
// ignore...
}
In controller, it also throws an exception:
if (ArrayUtil::isIndexExceed($list, $maxIndex)) {
// Index exceeds
throw new App\Exceptions\ExceedingIndexException;
}
However, when the error occurs, the Handler::render() is not invoked. The response is the ExceedingIndexException stack.
The following part is Exception class
My custom exception class, ExceedingIndexException:
namespace App\Exceptions;
use App\Http\Responses\Error;
use App\Exceptions\AbstractException;
class ExceedingIndexException extends AbstractException
{
public function __construct()
{
$message = 'Unable to execute';
$error = new Error('exceeding_index_value');
$statusCode = 400;
parent::__construct($statusCode, $error, $message);
}
}
The ExceedingIndexException class inherits AbstractException:
namespace App\Exceptions;
abstract class AbstractException extends \Exception
{
protected $statusCode;
protected $errors;
public function __construct(
$statusCode, $errors, $message, $code = 0, \Exception $previous = null) {
parent::__construct($message, $code, $previous);
$this->statusCode = $statusCode;
$this->errors = $errors;
}
public function getStatusCode()
{
return $this->statusCode;
}
public function getErrors()
{
return $this->errors;
}
}
Solution
I found my project depends on Dingo API for RESTful API. Because, Dingo also supports and registers its own exception handler, App\Exceptions\Handler doesn't be invoked.
I tried to use Custom Exception Responses in Dingo API as my Exception Handler to response error in JSON format. And it works for me.
Actually, you can replace the Dingo API error handler by a custom error handler. Get a copy of https://github.com/KIVagant/api/blob/develop/src/Exception/Handler.php
and save it to YourApp\Exceptions\Api\V1\Handler.php. Add interface Dingo\Api\Contract\Debug\ExceptionHandler to it,
then follow the instruction in Exceptions now can return any additional data.
use Dingo\Api\Contract\Debug\ExceptionHandler as DingoExceptionHandler;
class Handler implements ExceptionHandler, DingoExceptionHandler {
Replace the error handler, eg in boot ().
// Resolve YourApp\Exceptions\Api\V1\Handler ifself
$this->app->alias('api.exception', 'YourApp\Exceptions\Api\V1\Handler');
$this->app->singleton('api.exception', function ($app) {
return new \YourApp\Exceptions\Api\V1\Handler($app['Illuminate\Contracts\Debug\ExceptionHandler'],
$app['config']['api.errorFormat'], $app['config']['api.debug']);
});
Don't forget set error format. You can set up error format in boot(), eg:
// Set up error format
$this->app['api.exception']->setErrorFormat(...)
Indeed, Dingo API took over the Exception handling.
You you face similar problems where Lumen Exception handler isn't handling anything, probably some package took over the handling. In this case it was Dingo API.
Register your custom response for the exception for Dingo API:
https://github.com/dingo/api/wiki/Errors-And-Error-Responses#custom-exception-responses

Symfony2 - Flash messages when redirects to the login page

I have the automated redirect on login page when the route /user/* is accessed.
I need to display a flash messages when redirects to the login page.
I read something about Event Listeners but need a real example to implement that.
I was trying:
services:
listener.requestresponse:
class: SciForum\Version2Bundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
And my ExceptionListener
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// You get the exception object from the received event
$exception = $event->getException();
$message = sprintf(
'My Error says: %s with code: %s',
$exception->getMessage(),
$exception->getCode()
);
// Customize your response object to display the exception details
$response = new Response();
$response->setContent($message);
// HttpExceptionInterface is a special type of exception that
// holds status code and header details
if ($exception instanceof HttpExceptionInterface) {
$response->setStatusCode($exception->getStatusCode());
$response->headers->replace($exception->getHeaders());
} else {
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
}
// Send the modified response object to the event
$event->setResponse($response);
}
}
But the exception is newer throw when the automated redirect is there.
An EventListener is designed to listen for specific events. You have created an ExceptionListener, which is taking GetResponseForExceptionEvent as an argument. If the redirect is successful, then it won't ever throw any exception.
You need to create a generic EventListener, or even a InteractiveLoginEvent listener:
Here is a login listener I made:
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Core\SecurityContext;
use Doctrine\Bundle\DoctrineBundle\Registry as Doctrine;
class LoginListener
{
private $securityContext;
private $em;
public function __construct(SecurityContext $securityContext, Doctrine $doctrine)
{
$this->securityContext = $securityContext;
$this->em = $doctrine->getManager();
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
//do stuff
}
}
But, to solve your problem directly, could you not just get the redirect headers in the controller and then display the message?

Symfony 2 redirecting out of private function

In my Application I'm using a init function to init an action
the init function validate the user input
(for example the user is looking for an product what not exist -> the init function should redirect him to an errorpage "product ... not found")
/**
* #Route("/route/{var}", name="xyzbundle_xyz_index")
* #Template("VendorXyzBundle:xyz:index.html.twig")
*/
public function indexAction ($var)
{
$xyz = $this->initxyz($var);
...
.. more code
.
}
And there is a private function in this controller that should validate the from url given parameter and if it is wrong (dont exist in database etc), the private function should redirect
private function init($var)
{
if($this->databasesearchforexyz($var)){
// redirect to Errorpage (No xyz found named ...)
return $this->redirect($this->generateUrl('xyz_error_...'));
}
if($this->checksomethingelse($var)){
// redirect to some other error page
}
}
Please note, these are not my real method/variable/path/etc. names.
The problem is, it is not redirecting.
You can check if the init function returns an actual response, then you can return it directly from the main code. Like this:
public function indexAction ($var)
{
$xyz = $this->initxyz($var);
if ($xyz instanceof \Symfony\Component\HttpFoundation\Response) {
return $xyz;
}
...
.. more code
.
}
Btw, if you only need to check database existance you can use symfony's paramconverter
Here's some suggestion.
Return true from the init function if there's no redirect and return false if there's a redirect.
Example:
private function init($var) {
if ($error) {
// An error occurred, redirect
$this->redirect($this->generateUrl('xyz_error_...'));
return false;
}
// Else, everything alright
return true;
}
public function indexAction ($var) {
if (!$this->init($var)) {
// Failed to init, redirection happening
return;
}
// Continue as normal
}
Using the answer of #alex88, I aggregate an exception and an exception listener to do the redirect. That avoid me to repeat the condition over and over again, because my function could redirect the user under different scenarios.
1. Controller
namespace AppBundle\Controller;
use AppBundle\Exception\UserHasToBeRedirectedException;
class DefaultController extends Controller
{
public function indexAction(...)
{
...
$this->userHasToBeRedirected();
...
}
private function userHasToBeRedirected()
{
...
if ($userHasToBeRedirected) {
$response = $this->redirect($this->generateUrl(...));
throw new UserHasToBeRedirectedException($response);
}
...
}
}
2. Exception
namespace AppBundle\Exception;
use Exception;
use Symfony\Component\HttpFoundation\Response;
class UserHasToBeRedirectedException extends Exception
{
private $response;
public function __construct(Response $response)
{
$this->response = $response;
}
public function getResponse()
{
return $this->response;
}
public function setResponse(Response $response)
{
$this->response = $response;
return $this;
}
}
3. Exception Listener
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use AppBundle\Exception\UserHasToBeRedirectedException;
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
...
if ($exception instanceof UserHasToBeRedirectedException) {
$response = $exception->getResponse();
$event->setResponse($response);
}
...
}
}
4. Register the service at service.yml
...
appBundle.exception_listener:
class: AppBundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception }
...
For more information:
Symfony Documantation about Events

Categories