17

I am building a REST API using Symfony2, Doctrine, FOSRestBundle and JMSSerializer.

The issue I am having is when serializing my entities, the serializer pulls in any related entities. Eg for a task that is part of a story which is part of a board, so when serializing the task I get output that includes the story which includes the board, which then includes all other stories on the board.

Is there an easy way to limit this, and just include the foreignIds instead?

WayneC
  • 5,569
  • 2
  • 32
  • 43
  • I think quite the same question has ben posted here : http://stackoverflow.com/questions/6706485/how-to-encode-doctrine-entities-to-json-in-symfony-2-0-ajax-application . You will find some answers using JMSSerializer or not. – Julien Fastré Aug 13 '12 at 10:01

5 Answers5

19

Use JMS exclusion policy.

Example using annotations on category entity, where you don't want to include children and product related entities to be included:

use ...
    JMS\SerializerBundle\Annotation\ExclusionPolicy,
    JMS\SerializerBundle\Annotation\Exclude,
    ...;

/**
 * ...
 * @ExclusionPolicy("none")
 */
class Category
{
   /**
    * ...
    * @Exclude
    */
   private $children;

   /**
    * ...
    * @Exclude
    */
   private $products;

}

Look at the JMSSerializer docs for more information.

EDIT:

For example you could use partial keyword to select only data that you need. Although I could not, for the life of me, disable the loading of the full related entities (two levels down) if I pass entity object to the serializer (even when disabling load in DoctrineProxyHandler), but if I use an array, than it doesn't use doctrine lazy loading though proxies (as expected ofc).

Example using your example entities:

$dql = "SELECT t, s, partial b.{id}, partial ss.{id}
        FROM Acme\AppBundle\Entity\Task t
        JOIN t.story s
        JOIN s.board b
        JOIN b.stories ss"

$q = $this->_em-createQuery($dql);

$result = $q->getArrayResult();

This way you would get something like:

[
{
    id: 33,
    title: "My Task",
    story: [
    {
        id: 554,
        board: [
        {
            id: 14,
            stories: [
            {
                id: 554
            },
            {
                id: 3424
            },
            {
                id: 3487
            }
            ]
        }
        ]
    }
    ]

}
]

P.S. I'm actually intrigued by this "problem". Anyway I'll see to come up with solution to how to serialize entity object without using array result.

Marko Jovanović
  • 2,647
  • 3
  • 27
  • 36
  • 3
    Friendly Update: Since [ver 0.11](https://github.com/schmittjoh/JMSSerializerBundle/blob/master/UPGRADING.md#upgrading-from-010-to-011) JMS Seralizer core was extracted out of the bundle so instead of `JMS\SerializerBundle\Annotation\ExclusionPolicy` it will be `JMS\Serializer\Annotation\ExclusionPolicy` and `JMS\SerializerBundle\Annotation\Exclude` will be `JMS\Serializer\Annotation\Exclude`. – Kirill Fuchs Apr 26 '13 at 06:01
9

Just an update in the latest version of JMSSerializer, the place you should look at is

JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber

instead of

Serializer\Handler\DoctrineProxyHandler

To override the default lazy load behaviour, one should define his own event subscriber.

In your app/config.yuml add this:

parameters:
    ...
    jms_serializer.doctrine_proxy_subscriber.class: Your\Bundle\Event\DoctrineProxySubscriber

you can copy the class from JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber to Your\Bundle\Event\DoctrineProxySubscriber and comment out the $object->__load(); line

public function onPreSerialize(PreSerializeEvent $event)
{
    $object = $event->getObject();
    $type = $event->getType();

    // If the set type name is not an actual class, but a faked type for which a custom handler exists, we do not
    // modify it with this subscriber. Also, we forgo autoloading here as an instance of this type is already created,
    // so it must be loaded if its a real class.
    $virtualType = ! class_exists($type['name'], false);

    if ($object instanceof PersistentCollection) {
        if ( ! $virtualType) {
            $event->setType('ArrayCollection');
        }

        return;
    }

    if ( ! $object instanceof Proxy && ! $object instanceof ORMProxy) {
        return;
    }

     //$object->__load(); Just comment this out

    if ( ! $virtualType) {
        $event->setType(get_parent_class($object));
    }
}

Update: I ended up writing my own simplified version of serialisation tool: https://github.com/dlin-me/array-converter-bundle

David Lin
  • 13,168
  • 5
  • 46
  • 46
  • 1
    I like this solution, however it's not working for me. I created my own DoctrineProxySubscriber and checked it's used during serialization, but never reaches commented line and always lazy loads related entities. I don't want serializer to make queries to DB, but on the other hand I want it to serialize data that is already fetched. I would be grateful for any ideas. – Radzikowski May 22 '14 at 17:26
  • I've commented the line $object->__load and things went **almost** like I needed it... now it returns `id_thing : { id_thing : 1}` insetead the object.. is there any way to return `id_thing : 1` only? – KnF Sep 12 '14 at 05:59
  • Ah, another thing... I am not using Symfony2.. just the JMSSerializer and Doctrine.. How do I create the custom subscriber? – KnF Sep 12 '14 at 06:00
  • @KnF zf2? if so add `'jms_serializer.doctrine_proxy_subscriber' => 'ApplicationRest\EventDispatcher\Subscriber\DoctrineProxySubscriber'` under service_manager['invokables'] to your module.config.php – Marcel Djaman Apr 23 '15 at 20:42
  • @MarcelDjaman nope, not using any framework.. Just doctrine and JMSSerializer with Composer.. – KnF Apr 28 '15 at 22:28
  • Got it.. `$serializer = JMS\Serializer\SerializerBuilder::create()->configureListeners(function(EventDispatcher $dispatcher) {$dispatcher->addSubscriber(new \MyBundle\ProxySubscriber($this->container));})->build();` – KnF Apr 28 '15 at 22:41
8

Check the Serializer/Handler/DoctrineProxyHandler.php file on JMSSerializerBundle. Now, if you comment this line:

public function serialize(VisitorInterface $visitor, $data, $type, &$handled)
    {
        if (($data instanceof Proxy || $data instanceof ORMProxy) && (!$data->__isInitialized__ || get_class($data) === $type)) {
            $handled = true;

            if (!$data->__isInitialized__) {
                //$data->__load();
            }

It will stop lazy loading your entities. If this is what you're looking for, then just go ahead and create your own handler where you don't lazy load.

If this isn't correct, I recommend that you customize your entities before sending them to JMSSerializerBundle at your taste. For example, in any related entities I want the ID, while in others i need a custom column name like code, or name, or anything.

I just create a copy of my entity object and then start getting the fields I need for relationships. Then, I serialize that copy. JMSSerializerBundle won't lazy load because I already provided the proper fields.

Rakesh Sankar
  • 9,337
  • 4
  • 41
  • 66
Klaus S.
  • 1,239
  • 10
  • 18
  • Sounds like it could work for me, haven't had a chance to work on this this week but will take a look this weekend. – WayneC Aug 16 '12 at 08:44
  • Already figured this out a few months ago. Thanks filching this. http://stackoverflow.com/questions/11575345/disable-doctrine-2-lazy-loading-when-using-jms-serializer – Christian Huber Oct 16 '12 at 21:49
0

Here's a function to select the IDs of one-to-one or one-to-many associated entities in a generic way without using joins.

function selectWithAssociations($doctrine, $className) {

    $em = $doctrine->getManager();
    $meta = $em->getClassMetadata($className);

    //explicitly get IDs of associated entities
    $assocClauses = array();
    foreach ($meta->getAssociationMappings() as $assocName => $assoc) {
        if (isset($assoc['joinTable'])) {
            //todo: doesn't handle many to many associations
        } else {
            $assocClauses[] = ", IDENTITY(e.$assocName) AS $assocName";
        }
    }

    //run custom DQL query
    $q = $em->createQuery('SELECT e AS _d' . implode('', $assocClauses) . ' FROM ' . $className . ' e');
    $result = $q->getArrayResult();

    return $result;
}
Tamlyn
  • 22,122
  • 12
  • 111
  • 127
0

Here is the class which prevent lazy loading of one or many associations which can be used as JMS Serializer ExclusionStrategy.

use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\Proxy;
use JMS\Serializer\Context;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\SerializationContext;

/**
 * Class OnlyLoadedAssociationsExclusionStrategy
 *
 * http://stackoverflow.com/questions/11851197/avoiding-recursion-with-doctrine-entities-and-jmsserializer
 */
class OnlyLoadedAssociationsExclusionStrategy implements ExclusionStrategyInterface
{
    public function shouldSkipClass(ClassMetadata $metadata, Context $context)
    {
    }

    public function shouldSkipProperty(PropertyMetadata $property, Context $context)
    {
        if ($context instanceof SerializationContext){
            $vistingSet=$context->getVisitingSet();

            //iterate over object to get last object
            foreach ($vistingSet as $v){
                $currentObject=$v;
            }

            $propertyValue=$property->getValue($currentObject);

            if ($propertyValue instanceof Proxy){
                // skip not loaded one association
                if (!$propertyValue->__isInitialized__){
                    return true;
                }
            }

            if ($propertyValue instanceof PersistentCollection){
                // skip not loaded many association
                if (!$propertyValue->isInitialized()){
                    return true;
                }
            }
        }
        return false;
    }
}

Usage example:

$serializationContext->addExclusionStrategy(
     new OnlyLoadedAssociationsExclusionStrategy()
);
  • Also posted here: http://stackoverflow.com/questions/11575345/disable-doctrine-2-lazy-loading-when-using-jms-serializer – Pavel Sokolov Aug 24 '15 at 15:31
  • You should be avoiding copy&pasting answers across multiple questions. When you do this, [a flag is automatically raised for moderator attention](http://meta.stackoverflow.com/questions/270311/can-we-auto-flag-answers-that-are-clearly-copy-pasted/270341#270341). Instead, just one of the questions as a duplicate of the other. – CubeJockey Aug 24 '15 at 15:54