JMSSerializerBundle与抽象类 - Symfony2

I want to serialize and deserialize an entity with its dependancies but I am unable to serialize elements that concern an abstract class.

Hierarchy :

Test --> several Calls where Call class is an abstract class and is extended by TestCallExecuteQuery (same issue with $conditions)

Test.php :

/**
 * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository")
 * @ORM\Table(name="cfa_test")
 * @JMSSer\ExclusionPolicy("all")
 */
class Test
{

    /**
     * @ORM\OneToMany(targetEntity="TestCall", mappedBy="test", cascade={"all"}, orphanRemoval=true)
     * @JMSSer\Expose
     * @JMSSer\Groups({"export"})
     * @JMSSer\Type("ArrayCollection<App\Bundle\CapFileAnalyzerBundle\Entity\TestCall>")
     */
    protected $calls;

    /**
     * @ORM\OneToMany(targetEntity="TestCondition", mappedBy="test", cascade={"all"}, orphanRemoval=true)
     * @JMSSer\Expose
     * @JMSSer\Groups({"export"})
     * @JMSSer\Type("ArrayCollection<App\Bundle\CapFileAnalyzerBundle\Entity\TestCondition>")
     */
    protected $conditions;

TestCall.php :

/**
 * @ORM\Entity
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\Table(name="cfa_test_call")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 * @ORM\DiscriminatorMap({
 *      "executeQuery" = "App\Bundle\CapFileAnalyzerBundle\Entity\TestCallExecuteQuery",
 *      "call" = "App\Bundle\CapFileAnalyzerBundle\Entity\TestCall"
 * })
 * @JMSSer\ExclusionPolicy("all")
 * @JMSSer\Discriminator(field="serializedType", map={
 *      "executeQuery"="App\Bundle\CapFileAnalyzerBundle\Entity\TestCallExecuteQuery",
 *      "call" = "App\Bundle\CapFileAnalyzerBundle\Entity\TestCall"
 * })
 */
abstract class TestCall
{
    /**
     * @JMSSer\Expose
     * @JMSSer\Groups({"export"})
     */
    protected $type = 'call';

    /**
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Test", inversedBy="calls")
     */
    protected $test;
  /**
     * @JMSSer\VirtualProperty()
     * @JMSSer\SerializedName("serializedType")
     */
    public function getDiscr()
    {
        return $this->type;
    }

TestCallExecuteQuery.php :

/**
 * @ORM\Entity
 * @JMSSer\ExclusionPolicy("all")
 */
class TestCallExecuteQuery extends TestCall
{

    protected $type = 'executeQuery';

    /**
     * @ORM\Column(name="`query`", type="text")
     * @JMSSer\Expose
     * @JMSSer\Groups({"export"})
     */
    protected $query;

    /**
     * @ORM\Column(name="`return`", type="string", nullable=true)
     * @JMSSer\Expose
     * @JMSSer\Groups({"export"})
     */
    protected $return;

So I followed instructions found over Internet :

  • @JMSSer\Expose annotation with @JMSSer\ExclusionPolicy("all") in every class
  • @JMSSer\Discriminator annotation on top of abstract class TestCall to map with extender class (TestcallExecuteQuery)

But.. When I serialize I get only type property of TestCall but not query or return property defined in TestCallExecuteQuery :

{"tests":[{"calls":[{"type":"executeQuery"},{"type":"executeQuery"}], ... }

I know it's possible because I got them ONCE but I was unable to reproduce that even by turning back the clock..

{"tests":[{"calls":[{"query":"SELECT * FROM table","return":"return_1"}], ... }

EDIT :

Ok I probably got query and return by changing in Test.php :

/**
 * @JMSSer\Type("ArrayCollection<App\Bundle\CapFileAnalyzerBundle\Entity\TestCall>")
 */
    protected $calls;

To :

/**
 * @JMSSer\Type("ArrayCollection<App\Bundle\CapFileAnalyzerBundle\Entity\TestCallExecuteQuery>")
 */
    protected $calls;

What am I doing wrong ?

Ok so ! I found a solution after losing my mind during several days !

The solution is to create two event listeners PreSerialize and PostSerialize.

First of all I removed this part in TestCall.php (Abstract Class) :

/**
 * @JMSSer\VirtualProperty()
 * @JMSSer\SerializedName("serializedType")
 */
public function getDiscr()
{
    return $this->type;
}

And added these annotations in TestCallExecuteQuery.php (extender class) :

/**
 * @JMSSer\Type("string")
 * @JMSSer\Expose
 * @JMSSer\Groups({"export"})
 */
protected $type = 'executeQuery';

My listener looks like that :

<?php

namespace App\Bundle\CapFileAnalyzerBundle\EventListener;

use JMS\Serializer\EventDispatcher\Events;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;

class JMSSerializerListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            ['event' => Events::PRE_SERIALIZE, 'method' => 'onPreSerialize'],
            ['event' => Events::POST_SERIALIZE, 'method' => 'onPostSerialize']
        ];
    }

    /**
     * @param PreSerializeEvent $event
     */
    public function onPreSerialize(PreSerializeEvent $event)
    {
        $object = $event->getObject();

        if (is_object($object) &&
            is_subclass_of($object, 'App\Bundle\CapFileAnalyzerBundle\Entity\TestCall') &&
            get_class($object) !== $event->getType()['name']
        ) {
            $event->setType(get_class($event->getObject()));
        }
    }

    /**
     * @param ObjectEvent $event
     */
    public function onPostSerialize(ObjectEvent $event){
        $object = $event->getObject();
        if (is_object($object) &&
            is_a($object, 'App\Bundle\CapFileAnalyzerBundle\Entity\TestCallExecuteQuery')) {
            $event->getVisitor()->addData("serializedType", $object->getType());
        }
    }
}

Declaration of listener :

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>      
        <parameter key="cfa.events.jmsserializer_listener.class">App\Bundle\CapFileAnalyzerBundle\EventListener\JMSSerializerListener</parameter>
    </parameters>

    <services>
         <service id="cfa.events.jmsserializer_listener" class="%cfa.events.jmsserializer_listener.class%">
            <tag name="jms_serializer.event_subscriber"/>
        </service>

    </services>
</container>

I explain a little :

  1. PreSerailize Event

If object to serialize is a subclass of my Abstract class (TestCall in my case) I have to force the type of the event object to be serialized into the concerned subclass (TestCallExecuteQuery in my case). In fact, right object (TestCallExecuteQuery) is passed but it's mapped with its parent class (Abstract class TestCall)

Dump of $event object :

PreSerializeEvent {#977 ▼
  -object: TestCallExecuteQuery {#981 ▼
    #type: "executeQuery"
    #query: "SELECT * FROM table_name"
    #return: "return_3"
    #id: 2
    #test: Test {#948 ▶}
  }
  #type: array:2 [▼
    "name" => "App\Bundle\CapFileAnalyzerBundle\Entity\TestCall"
    "params" => []
  ]
  -context: SerializationContext {#420 ▶}
}
  1. PostSerialze Event

If serialized object is my child class I add a visitor property (not a virtual even custom)..

NB : JMSSerializeBundle does not serialize "virtual" property directly added to class for example by a method like that :

public function createProperty($name, $value) {    
     $this->{$name} = $value;    
}

Maybe Discriminator/Virtual Property are added like that in JMSSerializerBundle and that's why they are not serialized.. Don't really know.