Dynamically remove entities inside a JMS serializer event subscriber - symfony

I have a Doctrine entity and I use JMS serializer to render it in my API.
I'd like to add a boolean field like this :
/**
* #var bool
*
* #ORM\Column(name = "is_serialized", type = "boolean")
*/
protected $isSerialized = true;
I also use an EventSubscriber to add some data to my entity before serialization.
I'd like to dynamically include or not each entity, based on the $isSerialized value (I can't modify the Doctrine Query).
class SerializationEventSubscriber extends EventSubscriberInterface
{
/**
* #param ObjectEvent $event
*/
public function onPostSerialize(ObjectEvent $event)
{
if (!$this->isGroup('api', $event)) {
return;
}
$entity = $event->getObject();
$visitor = $event->getVisitor();
if (!$object->isSerialized()) {
// Skip the current object and remove it from serialization
}
}
}
I can't find any information about this, neither in the JMS annotation documentation.

Here is my EventListener, but instead of removing the object I just skip nulled field.
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\Metadata\ClassMetadata as JMSClassMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
class EntitySerializerListener
{
/**
* #var EntityManagerInterface
*/
protected $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
public function onPostSerialize(ObjectEvent $event)
{
/** #var JsonSerializationVisitor $visitor */
$object = $event->getObject();
$visitor = $event->getVisitor();
$context = $event->getContext();
$type = $event->getType();
/** #var JMSClassMetadata $metadata */
$metadata = $context->getMetadataFactory()->getMetadataForClass($type['name']);
$data = $visitor->endVisitingObject($metadata, $object, $type);
$visitor->startVisitingObject($metadata, $object, $type);
// Here I remove unnecessary fields
$this->prune($type['name'], $data);
// Reset fresh serialized data
foreach ($data as $field => $value) {
$visitor->visitProperty(new StaticPropertyMetadata($type['name'], $field, $value), $value);
}
}
/**
* Prune the empty field which was set to NULL by MaxDepth annotation but left in the data graph by JWT serializer.
*
* #param string $fqcn
* #param array $data
*/
protected function prune(string $fqcn, array & $data)
{
/** #var ClassMetadata $metadata */
$metadata = $this->em->getMetadataFactory()->getMetadataFor($fqcn);
// Handle association
$associations = $metadata->getAssociationMappings();
foreach ($associations as $field => $association) {
if (!array_key_exists($field, $data)) {
continue;
}
// Here remove entity or any other field which you want
if (empty($data[$field])) {
unset($data[$field]);
} else {
$this->prune($association['targetEntity'], $data[$field]);
}
}
}
}

Related

Symfony 4.4 Event Listener Error (MongoDB)

I'm trying to create a listener to when a new Rating is created. I followed all the documentation but I keep getting the same error:
Argument 1 passed to "Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch()" must be an instance of "Symfony\Component\EventDispatcher\Event", "App\Event\AverageRatingEvent" given.
I'm trying to use Symfony\Component\EventDispatcher\Event in the event but it keeps saying that it is deprecated and according to documents to use Symfony\Contracts\EventDispatcher\Event instead.
I register my event in the services and the following is my event, eventlistener and class
Class Rating
class RatingApiController extends AbstractController
{
/**
* #Route("api/rating/create", name="CreateRating", methods={"POST"})
* #param DocumentManager $dm
* #param Request $request
* #param EventDispatcher $eventDispatcher
* #return RedirectResponse|Response
* #throws MongoDBException
*
*/
public function addRating(Request $request, EventDispatcherInterface $eventDispatcher)
{
$response = [];
$form = $this->
createForm(RatingType::class, new Rating() ,array('csrf_protection' => false));
$request = json_decode($request->getContent(), true);
$form->submit($request);
if($form->isSubmitted() && $form->isValid())
{
$rating = $form->getData();
$this->documentManager->persist($rating);
$this->documentManager->flush();
$averageRatingEvent = new AverageRatingEvent($rating);
$eventDispatcher->dispatch( AverageRatingEvent::NAME, $averageRatingEvent);
$status = 200;
$response = ["status" => $status, "success" => true, "data" => $rating->getId()];
// return $this->redirectToRoute('rating_list');
}
}
Event
<?php
namespace App\Event;
use App\Document\Rating;
use Symfony\Contracts\EventDispatcher\Event;
class AverageRatingEvent extends Event
{
/**
* #var Rating $rating
*/
protected $rating;
public const NAME = "average.rating";
public function __construct(Rating $rating)
{
$this->rating = $rating;
}
public function getRating()
{
return $this->rating;
}
}
Listener
<?php
namespace App\Event;
use App\Document\Rating;
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
class AverageRatingListener
{
public function postPersist(LifecycleEventArgs $args)
{
$document = $args->getObject();
if(!$document instanceof Rating)
return;
}
public function RatingCreated()
{
dump("Hello a rating was just added");
}
}
Inside AverageRatingEvent you extend Event.
The use needs to be changed from
use Symfony\Contracts\EventDispatcher\Event;
to
use Symfony\Component\EventDispatcher\Event;

JMSSerializerBundle deserialization skip groups exclusion on id property using DoctrineObjectConstructor

I'm using jms/serializer-bundle 2.4.3 on a symfony 4.2 and a I noticed an annoying problem in my application :
when I post an entity, the DoctrineObjectConstructor uses id in content to retrieve another entity and thus patch it while it is excluded by my security groups
see rather entity
class Entity
{
/**
* #var int
*
* #ORM\Column(name="id", type="int")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #Serializer\Groups({"GetEntity"})
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
* #Serializer\Groups({"GetEntity", "PostEntity"})
*/
private $name;
}
controller
/**
* #Route("/entity", name="post_entity", methods={"POST"})
*/
public function postEntity(Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): JsonResponse
{
$deserializationContext = DeserializationContext::create();
$deserializationContext->setGroups(['PostEntity']);
$entity = $serializer->deserialize($request->getContent(), Entity::class, 'json', $deserializationContext);
$entityManager->persist($entity);
$entityManager->flush();
return $this->json($entity, Response::HTTP_OK, [], ['groups' => ['GetEntity']]);
}
I have some JMS configurations changes in services
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: true
jms_serializer.unserialize_object_constructor:
class: App\Serializer\ObjectConstructor
If anyone can explain to me how to ignore the id in this case I'm open to any suggestions.
Regards and thanks for any help
To resolve, just add override in your services.yaml
jms_serializer.doctrine_object_constructor:
class: App\Serializer\DoctrineObjectConstructor
arguments:
- '#doctrine'
- '#jms_serializer.unserialize_object_constructor'
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
and add a local DoctrineObjectConstructor updated to ignore entities without current deserialization group on id property
class DoctrineObjectConstructor implements ObjectConstructorInterface
{
const ON_MISSING_NULL = 'null';
const ON_MISSING_EXCEPTION = 'exception';
const ON_MISSING_FALLBACK = 'fallback';
private $fallbackStrategy;
private $managerRegistry;
private $fallbackConstructor;
/**
* Constructor.
*
* #param ManagerRegistry $managerRegistry Manager registry
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
* #param string $fallbackStrategy
*/
public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, $fallbackStrategy = self::ON_MISSING_NULL)
{
$this->managerRegistry = $managerRegistry;
$this->fallbackConstructor = $fallbackConstructor;
$this->fallbackStrategy = $fallbackStrategy;
}
/**
* {#inheritdoc}
*/
public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context)
{
// Locate possible ObjectManager
$objectManager = $this->managerRegistry->getManagerForClass($metadata->name);
if (!$objectManager) {
// No ObjectManager found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Locate possible ClassMetadata
$classMetadataFactory = $objectManager->getMetadataFactory();
if ($classMetadataFactory->isTransient($metadata->name)) {
// No ClassMetadata found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Managed entity, check for proxy load
if (!\is_array($data)) {
// Single identifier, load proxy
return $objectManager->getReference($metadata->name, $data);
}
// Fallback to default constructor if missing identifier(s)
$classMetadata = $objectManager->getClassMetadata($metadata->name);
$identifierList = [];
foreach ($classMetadata->getIdentifierFieldNames() as $name) {
$propertyGroups = [];
if ($visitor instanceof AbstractVisitor) {
/** #var PropertyNamingStrategyInterface $namingStrategy */
$namingStrategy = $visitor->getNamingStrategy();
$dataName = $namingStrategy->translateName($metadata->propertyMetadata[$name]);
$propertyGroups = $metadata->propertyMetadata[$name]->groups;
} else {
$dataName = $name;
}
if (!array_key_exists($dataName, $data) || true === empty(array_intersect($context->getAttribute('groups'), $propertyGroups))) {
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
$identifierList[$name] = $data[$dataName];
}
// Entity update, load it from database
$object = $objectManager->find($metadata->name, $identifierList);
if (null === $object) {
switch ($this->fallbackStrategy) {
case self::ON_MISSING_NULL:
return null;
case self::ON_MISSING_EXCEPTION:
throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));
case self::ON_MISSING_FALLBACK:
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
default:
throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
}
}
$objectManager->initializeObject($object);
return $object;
}
}

JMSSerializer deserialize entity by id

i'm using JMSSerializer to deserialize a JSON request and i'm having troubles with ManyToOne relations. I would like to deserialize the relation entity from a id given. Example:
Class Game {
/**
* #var Team
*
* #ORM\ManyToOne(targetEntity="Team")
* #ORM\JoinColumn(name="home_team_id", referencedColumnName="id")
* #JMSSerializer\SerializedName("home")
*/
private $homeTeam;
/**
* #ORM\ManyToOne(targetEntity="Team")
* #ORM\JoinColumn(name="visitor_team_id", referencedColumnName="id")
* #JMSSerializer\SerializedName("visitor")
*/
private $visitorTeam;
}
So when i get this Json
{"home": "id1", "visitor": "id2"}
Get the related entities. Any clouds?? i can't figure it out
Thanks in advance
Custom serializer handler allows to do it.
At first, you need to create your own serialization handler. Something like this:
<?php
namespace AppBundle\Serializer\Handler;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\RegistryInterface;
use JMS\Serializer\Context;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\GenericDeserializationVisitor;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\VisitorInterface;
use JMS\Serializer\GraphNavigator;
class EntityHandler implements SubscribingHandlerInterface
{
/**
* #var RegistryInterface
*/
protected $registry;
/**
* #return array
*/
public static function getSubscribingMethods()
{
$methods = [];
foreach (['json', 'xml', 'yml'] as $format) {
$methods[] = [
'type' => 'Entity',
'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
'format' => $format,
'method' => 'deserializeEntity',
];
$methods[] = [
'type' => 'Entity',
'format' => $format,
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'method' => 'serializeEntity',
];
}
return $methods;
}
/**
* EntityHandler constructor.
* #param RegistryInterface $registry
*/
public function __construct(RegistryInterface $registry)
{
$this->registry = $registry;
}
/**
* #param VisitorInterface $visitor
* #param $entity
* #param array $type
* #param Context $context
* #return mixed
*/
public function serializeEntity(VisitorInterface $visitor, $entity, array $type, Context $context)
{
$entityClass = $this->getEntityClassFromParameters($type['params']);
if (!$entity instanceof $entityClass) {
throw new InvalidArgumentException(
sprintf("Entity class '%s' was expected, but '%s' got", $entityClass, get_class($entity))
);
}
$entityManager = $this->getEntityManager($entityClass);
$primaryKeyValues = $entityManager->getClassMetadata($entityClass)->getIdentifierValues($entity);
if (count($primaryKeyValues) > 1) {
throw new InvalidArgumentException(
sprintf("Composite primary keys does'nt supported now (found in class '%s')", $entityClass)
);
}
if (!count($primaryKeyValues)) {
throw new InvalidArgumentException(
sprintf("No primary keys found for entity '%s')", $entityClass)
);
}
$id = array_shift($primaryKeyValues);
if (is_int($id) || is_string($id)) {
return $visitor->visitString($id, $type, $context);
} else {
throw new InvalidArgumentException(
sprintf(
"Invalid primary key type for entity '%s' (only integer or string are supported",
$entityClass
)
);
}
}
/**
* #param GenericDeserializationVisitor $visitor
* #param string $id
* #param array $type
*/
public function deserializeEntity(GenericDeserializationVisitor $visitor, $id, array $type)
{
if (null === $id) {
return null;
}
if (!(is_array($type) && isset($type['params']) && is_array($type['params']) && isset($type['params']['0']))) {
return null;
}
$entityClass = $type['params'][0]['name'];
$entityManager = $this->getEntityManager($entityClass);
return $entityManager->getRepository($entityClass)->find($id);
}
/**
* #param array $parameters
* #return string
*/
protected function getEntityClassFromParameters(array $parameters)
{
if (!(isset($parameters[0]) && is_array($parameters[0]) && isset($parameters[0]['name']))) {
throw new InvalidArgumentException('Entity class is not defined');
}
if (!class_exists($parameters[0]['name'])) {
throw new InvalidArgumentException(sprintf("Entity class '%s' is not found", $parameters[0]['name']));
}
return $parameters[0]['name'];
}
/**
* #param string $entityClass
* #return EntityManagerInterface
*/
protected function getEntityManager($entityClass)
{
$entityManager = $this->registry->getEntityManagerForClass($entityClass);
if (!$entityManager) {
throw new InvalidArgumentException(
sprintf("Entity class '%s' is not mannaged by Doctrine", $entityClass)
);
}
return $entityManager;
}
}
Then you should register it in your service configuration file. If you use yaml, it will be something like that:
custom_serializer_handle:
class: AppBundle\Serializer\Handler\EntityHandler
arguments: ['#doctrine']
tags:
- {name: 'jms_serializer.subscribing_handler'}
In your entity, define JMSSerializer Type annotation
/**
* #var Team
* * #ORM\ManyToOne(targetEntity="Team")
* #ORM\JoinColumn(name="home_team_id", referencedColumnName="id")
* #JMSSerializer\SerializedName("home")
* #JMSSerializer\Type("Entity<AppBundle\Entity\Team>")
* List item
*/
private $homeTeam;
Don't forget clear caches.
That's all.

symfony2 custom annotation translation

Please help me to translate custom annotation.
I'm trying to translate #Render(title="Page"). Translate generator not found this, and title not traslate.
I try to understand how it is done in the component validation Symfony but nothing happens.
<?php
namespace Shooos\ProductBundle\Controller\Admin;
use Sensio\Bundle\FrameworkExtraBundle\Configuration as PRS;
use Shooos\CoreBundle\Controller\BaseController;
use Aft\RenderParkingBundle\Annotations as CA;
use Gedmo\Mapping\Annotation\Translatable;
/**
* #PRS\Route("/admin")
* Class CategoryController
* #package Shooos\ProductBundle\Controller\Admin
*/
class CategoryController extends BaseController
{
/**
* #CA\Render(title="Categories")
* #PRS\Route("/categories", name="admin.categories")
*/
public function indexAction()
{
}
}
<?php
namespace Aft\RenderParkingBundle\Annotations\Driver;
use Doctrine\Common\Annotations\Reader;
use Sensio\Bundle\FrameworkExtraBundle\Templating\TemplateGuesser;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Aft\RenderParkingBundle\Annotations;
use Symfony\Component\Translation\TranslatorInterface;
class AnnotationDriver
{
/**
* #var Reader
*/
private $reader;
/**
* #var TemplateGuesser
*/
private $guesser;
/**
* #var TranslatorInterface
*/
private $translator;
public function __construct(Reader $reader, TemplateGuesser $guesser, TranslatorInterface $translator)
{
$this->reader = $reader;
$this->guesser = $guesser;
$this->translator = $translator;
}
/**
* This event occurs when call any controller
*/
public function onKernelController(FilterControllerEvent $event)
{
/** Controller exists */
if (!is_array($controller = $event->getController())) {
return;
}
/**
* Controller
* #var \ReflectionObject $object
*/
$object = new \ReflectionObject($controller[0]);
$method = $object->getMethod($controller[1]);
foreach ($this->reader->getMethodAnnotations($method) as $configuration) {
if ($configuration instanceof Annotations\Render) {
$request = $event->getRequest();
$title = $this->translator->trans($configuration->getTitle());
$request->attributes->set('_page_title', $title);
if (null === $configuration->getTemplate()) {
$configuration->setTemplate(
$this->guesser->guessTemplateName(
$controller,
$request
));
}
$request->attributes->set('_page_template', $configuration->getTemplate());
}
}
}
}
On your annotation to object converter, where you inject the annotation reader, inject the translator service and translate the value at the transformation process, from annotation to object.
$description = $this->translator->trans($transformedAnnotationObject->getDescription());

Automatic translation before serialization in symfony2 with JMSSerializerBundle

I have some logic to apply after getting entities from database( by findAll() ) and before serializing the result to json.
I want to add translation on some fields. I know that I can do it manually by iterating on each entity and apply my logic in controller. But I need a better way to do it.
Is there a suggestions to make this automatic ?
I've got similar problem and tried to resolve this using custom handler but with no success, so i created compiler pass and override JsonSerializationVisitor where string values are serialized with TranslatableJsonSerializationVisitor class:
namespace tkuska\DemoBundle\Serialization;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\Context;
class TranslatableJsonSerializationVisitor extends JsonSerializationVisitor
{
/**
* #var \Symfony\Component\Translation\Translator;
*/
private $translator;
public function visitString($data, array $type, Context $context)
{
if (in_array('translatable', $type['params'])) {
return (string) $this->translator->trans($data);
}
return (string) $data;
}
public function setTranslator(\Symfony\Component\Translation\Translator $translator)
{
$this->translator = $translator;
}
}
and compiler:
namespace tkuska\DemoBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class TranslatableJsonSerializationCompiler implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$jmsJsonSerializationVisitor = $container->getDefinition('jms_serializer.json_serialization_visitor');
$jmsJsonSerializationVisitor->setClass('tkuska\DemoBundle\Serialization\TranslatableJsonSerializationVisitor');
$jmsJsonSerializationVisitor->addMethodCall('setTranslator', array(new Reference('translator')));
}
}
in entity i set annotation for type 'string' with param 'translatable'
/**
* #VirtualProperty
* #Groups({"details"})
* #Type("string<'translatable'>")
* #return order status
*/
public function getOrderStatus(){
return 'status.ordered';
}
Of course 'status.ordered' is my translation key.
Thank you #zizoujab. Very useful post. I made a small improvement to it to call parent method and to unset my parameters, so that i can use this way of altering data on more complex types like array, that already have parameters and used
/**
* #VirtualProperty
* #Groups({"details"})
* #Type("string<translatable>")
* #return order status
*/
instead of
/**
* #VirtualProperty
* #Groups({"details"})
* #Type("string<'translatable'>")
* #return order status
*/
to convert the string 'translatable' into a parameter name instead of parameter value thus you can pass more complex parameters like this, and allow me to unset this parameters before calling the parent method.
/**
* #VirtualProperty
* #Groups({"details"})
* #Type("string<translatable<set of parameters>>")
* #return order status
*/
Code:
<?php
namespace Mktp\DefaultBundle\Service\JmsSerializer;
use JMS\Serializer\Context;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Component\Translation\Translator;
class TranslatableJsonSerializationVisitor extends JsonSerializationVisitor
{
/**
* #var Translator;
*/
private $translator;
/**
* #param string $data
* #param array $type
* #param Context $context
* #return string
*/
public function visitString($data, array $type, Context $context)
{
$translatable = $this->getParameters('translatable', $type['params']);
if (count($translatable)) {
$data = (string)$this->translator->trans($data);
}
return parent::visitString($data, $type, $context);
}
/**
* #param array $data
* #param array $type
* #param Context $context
* #return array|\ArrayObject|mixed
*/
public function visitArray($data, array $type, Context $context)
{
$translatable = $this->getParameters('translatable', $type['params']);
if (count($translatable)) {
foreach ($data as $key => $value) {
if (is_string($value)) {
$data[$key] = (string)$this->translator->trans($value);
}
}
}
return parent::visitArray($data, $type, $context);
}
/**
* #param Translator $translator
*/
public function setTranslator(Translator $translator)
{
$this->translator = $translator;
}
/**
* #param string $type
* #param array $parameters
* #param bool $unsetParameters
* #return array
*/
protected function getParameters($type, &$parameters, $unsetParameters = true)
{
$result = array();
foreach ($parameters as $key => $parameter) {
if ($parameter['name'] == $type) {
$result[] = $parameter;
if ($unsetParameters) {
unset($parameters[$key]);
}
}
}
$parameters = array_values($parameters);
return $result;
}
}

Resources