FOSRestBundle
FOSRestBundle copied to clipboard
overwrite JMS's ExceptionHandler (FOS\RestBundle\Serializer\Normalizer\ExceptionHandler)
I am trying to create custom handlers (I am using JMS-Serializer) for different exceptions to make them all output a similar json structure response. I have successfully overwritten HttpException exceptions and it works like a charm.
I need some other exceptions that I tried to handle, but the handlers that I created are just ignored.
I created handlers for HttpException, OAuth2AuthenticateException, OAuth2ServerException, \Exception
all don't work except for the HttpException handler.
services.yaml
parameters:
#parameter_name: value
services:
jms_serializer.http_exception_handler:
class: AppBundle\Handler\HttpExceptionHandler
tags:
- { name: jms_serializer.subscribing_handler, priority: 999999999 }
arguments: ["@fos_rest.exception.messages_map", false]
jms_serializer.oauth_server_exception_handler:
class: AppBundle\Handler\OAuth2ServerExceptionHandler
tags:
- { name: jms_serializer.subscribing_handler, priority: 999999999 }
arguments: ["@fos_rest.exception.messages_map", false]
jms_serializer.oauth_authenticate_exception_handler:
class: AppBundle\Handler\OAuth2AuthenticateExceptionHandler
tags:
- { name: jms_serializer.subscribing_handler, priority: 999999999 }
arguments: ["@fos_rest.exception.messages_map", false]
jms_serializer.other_exception_handler:
class: AppBundle\Handler\ExceptionHandler
tags:
- { name: jms_serializer.subscribing_handler, priority: 999999999 }
arguments: ["@fos_rest.exception.messages_map", false]
note: I tried tags with and without priority: 999999999, both have same behaviour
content of my ExceptionHandler:
<?php
namespace AppBundle\Handler;
use FOS\RestBundle\Serializer\Normalizer\AbstractExceptionNormalizer;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\XmlSerializationVisitor;
class ExceptionHandler extends AbstractExceptionNormalizer implements SubscribingHandlerInterface
{
/**
* @return array
*/
public static function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => \Exception::class,
'method' => 'serializeToJson',
],
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'xml',
'type' => \Exception::class,
'method' => 'serializeToXml',
],
];
}
/**
* @param JsonSerializationVisitor $visitor
* @param Exception $exception
* @param array $type
* @param Context $context
*
* @return array
*/
public function serializeToJson(
JsonSerializationVisitor $visitor,
\Exception $exception,
array $type,
Context $context
) {
$data = $this->convertToArray($exception, $context);
return $visitor->visitArray($data, $type, $context);
}
/**
* @param XmlSerializationVisitor $visitor
* @param Exception $exception
* @param array $type
* @param Context $context
*/
public function serializeToXml(
XmlSerializationVisitor $visitor,
\Exception $exception,
array $type,
Context $context
) {
$data = $this->convertToArray($exception, $context);
if (null === $visitor->document) {
$visitor->document = $visitor->createDocument(null, null, true);
}
foreach ($data as $key => $value) {
$entryNode = $visitor->document->createElement($key);
$visitor->getCurrentNode()->appendChild($entryNode);
$visitor->setCurrentNode($entryNode);
$node = $context->getNavigator()->accept($value, null, $context);
if (null !== $node) {
$visitor->getCurrentNode()->appendChild($node);
}
$visitor->revertCurrentNode();
}
}
/**
* @param \Exception $exception
*
* @return array
*/
protected function convertToArray(\Exception $exception, Context $context)
{
$data = [];
$templateData = $context->attributes->get('template_data');
if ($templateData->isDefined()) {
$data['code1'] = $statusCode = $templateData->get()['status_code'];
}
$data['message1'] = $this->getExceptionMessage($exception, isset($statusCode) ? $statusCode : null);
return $data;
}
}
it's exactly similar to FOS\RestBundle\Serializer\Normalizer\ExceptionHandler.php the only difference is the data fields are code1, message1 instead of code, message
I tried also to uncomment all the other services:
services:
jms_serializer.other_exception_handler:
class: AppBundle\Handler\ExceptionHandler
tags:
- { name: jms_serializer.subscribing_handler, priority: 999999999 }
arguments: ["@fos_rest.exception.messages_map", false]
but the handler is never called. (of course I tried restarting the server and deleting the cache)
The behavior I excpected is that since I am making new handlers, they will be called in their order.
AppBundle\Handler\HttpExceptionHandler
AppBundle\Handler\OAuth2ServerExceptionHandler
AppBundle\Handler\OAuth2AuthenticateExceptionHandler
AppBundle\Handler\ExceptionHandler
FOS\RestBundle\Serializer\Normalizer\ExceptionHandler
the current behavior looks a bit weird, OAuth2ServerExceptionHandler & OAuth2AuthenticateExceptionHandler are never handeled
the order of other exceptions handling is
AppBundle\Handler\HttpExceptionHandler
FOS\RestBundle\Serializer\Normalizer\ExceptionHandler
AppBundle\Handler\ExceptionHandler
I could not find many topics regarding this topic, I hope someone can explain what's wrong with my code.
my AppKernel order
new JMS\SerializerBundle\JMSSerializerBundle(),
new AppBundle\AppBundle(),
// ...
new FOS\RestBundle\FOSRestBundle(),
// ..
my config.yml
jms_serializer:
handlers:
datetime:
default_format: "c" # ISO8601
default_timezone: "UTC"
property_naming:
separator: _
lower_case: true
metadata:
auto_detection: true
directories:
FOSUserBundle:
namespace_prefix: "FOS\\UserBundle"
path: "@AppBundle/Resources/config/serializer/fos"
fos_rest:
param_fetcher_listener: true
view:
view_response_listener: force
mime_types:
json: ['application/json', 'application/json;version=1.0', 'application/json;version=1.1']
view_response_listener: 'force'
formats:
xml: false
json: true
templating_formats:
html: true
format_listener:
rules:
- { path: "^/v[0-9]", priorities: [ json ], fallback_format: json, prefer_extension: true }
- { path: "^/oauth", priorities: [ json ], fallback_format: json, prefer_extension: true }
exception:
enabled: true
allowed_methods_listener: true
access_denied_listener:
json: true
body_listener: true
serializer:
serialize_null: true
I am not sure if it's going to make a difference, it's recommended to load JMSSerializerBundle before FOSRestBundle I tried to play with the order a little bit, but with no luck.
symfony version: v2.8.22
FOSRest-Bundle version: v2.2.0
JMS-Serilizer-Bundle version: v2.0.0
I tried dev-master version for both FOSRest-Bundle & JMS-Serilizer-Bundle as well.
still don't know how does it work, but when I named my service the same name as the FOSRestBundle's one it worked.
fos_rest.serializer.exception_normalizer.jms:
class: AppBundle\Handler\ExceptionHandler
tags:
- { name: jms_serializer.subscribing_handler }
arguments: ["@fos_rest.exception.messages_map", false]
@ebeem if you reused the same name, it replaced the service entirely
@stof so what can I do to handle/normalize an exception using JMS?
and why only HttpException is handled?
@ebeem Hi, how/where did you define your service with the same name as "fos_rest.serializer.exception_normalizer.jms"? because this service depends on service "@fos_rest.exception.messages_map" which is not public in this bundle
so far I can not find any way to override the jms exception handler without writing all the dependencies @stof is it possible to add some example in the documentation about how to customize exception handler/normalizer for people who use jms serilaizer?
Related to https://github.com/FriendsOfSymfony/FOSRestBundle/issues/1379
@phoenixgao services.yml
@phoenixgao private services can perfectly be used as a dependency (the container itself does not isolate services by bundle, there is no such concept). The only thing being forbidden for private services is runtime retrieval with $container->get('my_id') (this restriction is what allows us to optimize the container, as all other accesses to the service are known at compile time)
Perfect! Thanks! @ebeem @stof
Hello,
it seems that the bug still exists and I need to name my service fos_rest.serializer.exception_normalizer.jms in order to override the custom exception handler.
Can you confirm that?
Thanks.
@lukepass hey, sorry, but I switched to another framework (Java spring). I did not investigate the issue, but I think it's a bug.
I wonder if anyone else had that issue and managed to find a solution without overriding fos_rest.serializer.exception_normalizer.jms
I am still doing this in my recent projects!
You should declare priority of method in array returned from getSubscribingMethods:
<?php
namespace App\Serializer\Handler;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Exception;
class ExceptionSubscribingHandler implements SubscribingHandlerInterface
{
public static function getSubscribingMethods(): array
{
return [
[
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => Exception::class,
'method' => 'serializeToJson',
'priority' => -1, // defaults to 0, lowest priority gets used
],
];
}
public function serializeToJson(JsonSerializationVisitor $visitor, Exception $exception, array $type, Context $context)
{
return 'foo';
}
}
For anyone curious why - take a look at class CustomHandlersPass in JMSSerializerBundle.
Pretty old thread, but I thought to share another possible explanation/solution.
The SerializerErrorRenderer converts Throwables to FlattenException, so you cannot catch your own Exception class via 'type' => CustomException::class. You can either catch Exception directly or work with FlattenException. Combined with the priority flag mentioned by @hxv the following works now for me:
public function __construct(FlattenExceptionHandler $exceptionHandler) {
$this->exceptionHandler = $exceptionHandler;
}
public static function getSubscribingMethods() {
return [[
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'type' => FlattenException::class,
'format' => 'json',
'method' => 'serializeExceptionToJson',
'priority' => -1
]];
}
public function serializeExceptionToJson(JsonSerializationVisitor $visitor, FlattenException $exception, array $type, Context $context) {
if ($exception->getClass() !== CustomException::class) {
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context);
}
// ... your conversion code
}