40

I've an entity I usually serialize using the JMS Serializer bundle. I have to add to the serialization some fields that doesn't reside in the entity itself but are gathered with some db queries.

My idea was to create a custom object, fill the fields with the entity fields and add the custom one. But this seems a bit tricky and expensive to do for every variation (I use lot of serialization groups) of the class.

Is there a better/standard way to do this? Using a factory? Pre/Post serialization events?

Maybe I can listen for the serialization and checking entity type and serialization groups add the custom fields? But instead of making a query for each entity it would be better to gather all the data of the related entities and then add it to them. Any help is appreciated

alex88
  • 4,788
  • 4
  • 39
  • 60

6 Answers6

72

I've found the solution by myself,

to add a custom field after the serialization has been done we've to create a listener class like this:

<?php

namespace Acme\DemoBundle\Listener;

use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\Tag;
use JMS\DiExtraBundle\Annotation\Inject;
use JMS\DiExtraBundle\Annotation\InjectParams;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Acme\DemoBundle\Entity\Team;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\JsonSerializationVisitor;

/**
 * Add data after serialization
 *
 * @Service("acme.listener.serializationlistener")
 * @Tag("jms_serializer.event_subscriber")
 */
class SerializationListener implements EventSubscriberInterface
{

    /**
     * @inheritdoc
     */
    static public function getSubscribedEvents()
    {
        return array(
            array('event' => 'serializer.post_serialize', 'class' => 'Acme\DemoBundle\Entity\Team', 'method' => 'onPostSerialize'),
        );
    }

    public function onPostSerialize(ObjectEvent $event)
    {
        $event->getVisitor()->addData('someKey','someValue');
    }
}

That way you can add data to the serialized element.

Instead, if you want to edit an object just before serialization use the pre_serialize event, be aware that you need to already have a variable (and the correct serialization groups) if you want to use pre_serialize for adding a value.

alex88
  • 4,788
  • 4
  • 39
  • 60
  • 2
    Found out myself, do it in pre_serialize not post. – Geshan Aug 15 '13 at 12:55
  • @Geshan yup! sorry for the delay – alex88 Aug 17 '13 at 09:41
  • What about if the extra key should be exposed for certain groups? Is it possible to add @JMS\Groups property via this event listener? – werd Oct 01 '13 at 12:43
  • @werd I'm not totally sure about that, you have to find if the visitor or anything you can get from the event has the serialization group list. Else you can use the preserialize and set a field already inside the class you're serializing and put the group logic there. – alex88 Oct 01 '13 at 13:10
  • I found @VirtualProperty but for some reason it doesn`t seem to work. Any suggestions? – werd Oct 01 '13 at 13:26
  • How to get a data of the entity? Is there anything like ->getData? – iBet7o Oct 03 '13 at 11:57
  • @iBet7o not in post serialize afaik, just on preserialize where you can change entity data and get the full entity – alex88 Oct 03 '13 at 14:02
  • 2
    Note that it does not work for `XmlSerializationVisitor` – Touki Jan 09 '14 at 10:18
  • You should note, that this only works for some visitors, namely the subclasses of GenericSerializationVisitor. – apfelbox Oct 02 '14 at 10:32
  • 1
    be aware that addData is deprecated and setData has another context in pre and post serialization calls (pre is the parent object data and post is the object data) – pscheit Jun 28 '18 at 20:44
20

I am surprised why nobody have suggested a much more easier way. You need just to use @VirtualProperty:

<?php

// ...
/**
 * @JMS\VirtualProperty
 * @JMS\SerializedName("someField")
 */
public function getSomeField()
{
    return $this->getTitle() . $this->getPromo();
}
James Akwuh
  • 2,169
  • 1
  • 23
  • 25
  • 1
    Seems like an cool way to do it, but i need to have a generated route in my serialized object so i need to have container in entity but it seems like a bad practice... Any advices ? – Kaz Sep 09 '15 at 10:08
  • @Kaz then you need to create own Service for that purpose. Of course, using container out of Model is a bad practice due to separation of concerns – James Akwuh Sep 09 '15 at 10:24
11

To further answer the original question. Here is how you limit added data for some serialized groups (in this example some_data is only added when we are not using the list group:

public function onPostSerializeSomeEntityJson(ObjectEvent $event) {

    $entity = $event->getObject();

    if (!in_array('list', (array)$event->getContext()->attributes->get('groups'))) {

        $event->getVisitor()->addData('user_access', array(
            'some_data' => 'some_value'
        ));
    }
}

(array)$event->getContext()->attributes->get('groups') contains an array of the used serialized groups.

Petter Kjelkenes
  • 1,605
  • 1
  • 19
  • 25
  • 2
    Some updates for you great answer: $visitor = $event->getVisitor(); $attributes = $event->getContext()->attributes; $groups = $attributes->get('groups'); if ($groups instanceof Some) { if (in_array(Track::GROUP_TRACK_URL, $groups->get(), true)) { $visitor->addData('url', 'TODO add url'); } } – borN_free Apr 27 '15 at 17:02
6

The accepted answer only works when the visitor is derived from \JMS\Serializer\GenericSerializationVisitor. This means it will work for JSON, but fail for XML.

Here's an example method which will cope with XML. It looks at the interfaces the visitor object supports and acts appropriately. It shows how you might add a link element to both JSON and XML serialized objects...

public function onPostSerialize(ObjectEvent $event)
{
    //obtain some data we want to add
    $link=array(
        'rel'=>'self',
        'href'=>'http://example.org/thing/1',
        'type'=>'application/thing+xml'
    );

    //see what our visitor supports...
    $visitor= $event->getVisitor();
    if ($visitor instanceof \JMS\Serializer\XmlSerializationVisitor)
    {
        //do XML things
        $doc=$visitor->getDocument();

        $element = $doc->createElement('link');
        foreach($link as $name => $value) {
            $element->setAttribute($name, $value);
        }
        $doc->documentElement->appendChild($element);
    } elseif ($visitor instanceof \JMS\Serializer\GenericSerializationVisitor)
    {
        $visitor->addData('link', $link);
    }


}
Paul Dixon
  • 295,876
  • 54
  • 310
  • 348
4

What about this: http://jmsyst.com/libs/serializer/master/handlers

In summary, you define a class that receives an object and returns text or an array (that will be converted to json).

You have class "IndexedStuff" that contains a weird calculated field that for some reason should be calculated at serialization time.

<?php

namespace Project/Model;

class IndexedStuff
{
   public $name;
   public $value;
   public $rawData;
}

Now create the handler

<?php

namespace Project/Serializer;

use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\Context;

class MyHandler implements SubscribingHandlerInterface
{
    public function setEntityManager(Registry $registry) {
         // Inject registry instead of entity manager to avoid circular dependency
         $this->em = $registry->getEntityManager();
    }
    public static function getSubscribingMethods()
    {
        return array(
            array(
                'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => 'Project/Model/IndexedStuff',
                'method' => 'serializeIndexedStuffToJson',
            ),
        );
    }

    public function serializeIndexedStuffToJson(JsonSerializationVisitor $visitor, Project/Model/IndexedStuff $stuff, array $type, Context $context)
    {
        // Build your object here and return it
        $score = $this->em->find("ProjectBundle:Calculator", $stuff->value)
        return array("score" => $score->getIndexScore(), "name"=> $score->name
    }
}

Finally register the service

services:
  project.serializer.stuff:
      class: Project\Serializer\MyHandler
      calls:
        - [setEntityManager, ["@doctrine"]]

Now everywhere you want to serialize an object of type "IndexedStuff" you will get a json like this

{"name": "myName", "score" => 0.3432}

By this way you can fully customize how your entity is serialized

Álvaro García
  • 431
  • 2
  • 7
4

addData is deprecated so since 2.0.0, so we need to do it like this:

use JMS\Serializer\EventDispatcher\ObjectEvent;

class MySerializerHandler {

    public function onPostSerialize(ObjectEvent $event)
    {
        /** @var MySpecialObjectType $object */
        $myObject = $event->getObject();

        $key = 'customDataKey';
        $value = 'myvalue';

        $event->getVisitor()->visitProperty(
            new StaticPropertyMetadata('', $key, $value),
            $value
        );
    }
}

services.yaml

services:
    MySerializerHandler:
        tags:
          - { name: jms_serializer.event_listener, class: 'MySpecialObjectType', event: serializer.post_serialize, method: 'onPostSerialize' }

https://github.com/schmittjoh/serializer/blob/c9c82c841b8ebe682ca44972d64fded215f72974/UPGRADING.md#from-1130-to-200

pscheit
  • 2,882
  • 27
  • 29