1

I am trying to serialize a property which is a Doctrine Criteria:

public function getUserResults(User $user)
{
    $criteria = Criteria::create()
        ->where(Criteria::expr()->eq('user', $user))
    ;

    return $this->getResults()->matching($criteria);
}

I cannot use the @VirtualProperty because it needs an argument so I implemented a custom subscriber for one of my types following this post:

https://stackoverflow.com/a/44244747

<?php

namespace AppBundle\Serializer;

use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use JMS\Serializer\EventDispatcher\ObjectEvent;

class ExerciseSubscriber implements EventSubscriberInterface
{
    private $currentUser;

    public function __construct(TokenStorage $tokenStorage)
    {
        $this->currentUser = $tokenStorage->getToken()->getUser();
    }

    public static function getSubscribedEvents()
    {
        return array(
            array(
                'event' => 'serializer.post_serialize',
                'method' => 'onPostSerialize',
                'class' => Exercise::class, // if no class, subscribe to every serialization
                'format' => 'json', // optional format
            ),
        );
    }

    public function onPostSerialize(ObjectEvent $event)
    {
        if (!$this->currentUser) {
            return;
        }

        $exercise = $event->getObject();
        $visitor = $event->getVisitor();

        $results = $exercise->getUserResults($this->currentUser);
        dump($results); // <-- It is an ArrayCollection with many elements

        $visitor->setData(
            'my_user_results',
            $results // <-- when rendered is an empty {}
        );
    }
}

Unfortunately the user_results property is always empty!
I looked into the source code for the serializer and I found that:

/**
 * Allows you to add additional data to the current object/root element.
 * @deprecated use setData instead
 * @param string $key
 * @param integer|float|boolean|string|array|null $value This value must either be a regular scalar, or an array.
 *                                                       It must not contain any objects anymore.
 */
public function addData($key, $value)
{
    if (isset($this->data[$key])) {
        throw new InvalidArgumentException(sprintf('There is already data for "%s".', $key));
    }

    $this->data[$key] = $value;
}

Please note the It must not contain any objects anymore.

How can I solve this?

StockBreak
  • 2,857
  • 1
  • 35
  • 61

2 Answers2

3

Please try to use serializer.post_serialize event instead of serializer.pre_serialize one. Also your virtual property name (user_results) should be different from any existing serializable fields.

The second argument of setData method must either be a regular scalar, or an array. It must not contain any objects. I suggest to inject JMS serializer into your listener class to serialize your objects array into array of scalars.

<?php

namespace AppBundle\Serializer;

use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\Serializer;

class ExerciseSubscriber implements EventSubscriberInterface
{
    private $currentUser;
    private $serializer;

    public function __construct(TokenStorage $tokenStorage, Serializer $serializer)
    {
        $this->currentUser = $tokenStorage->getToken()->getUser();
        $this->serializer  = $serializer;
    }

    public static function getSubscribedEvents()
    {
        return array(
            array(
                'event'  => 'serializer.post_serialize',
                'method' => 'onPostSerialize',
                'class'  => Exercise::class, // if no class, subscribe to every serialization
                'format' => 'json', // optional format
            ),
        );
    }

    public function onPostSerialize(ObjectEvent $event)
    {
        if (!$this->currentUser) {
            return;
        }

        $exercise = $event->getObject();
        $visitor = $event->getVisitor();

        $visitor->setData(
            'user_results',
            $this->serializer->toArray($exercise->getUserResults($this->currentUser))
        );
    }
}
Mikhail Prosalov
  • 4,155
  • 4
  • 29
  • 41
  • I updated my answer with the correct event `post_serialize` but it doesn't work. Please see my last comment in bold. – StockBreak Aug 02 '17 at 12:35
  • Updated my answer. $results variable should not contain any objects. Please serialize it before calling the setData method. – Mikhail Prosalov Aug 02 '17 at 13:35
  • The problem is that `getUserResults` returns a list of `ExerciseResult`. If I serialize it before calling `setData` doesn't it appear as a serialized string (encoded)? – StockBreak Aug 04 '17 at 12:29
  • You should serialize it into an array. You can inject jms serializer into this listener class and use it to serialize ExerciseResult entities into array using "toArray" method. Or you can implement toArray method in your ExerciseResult entity to return array with important properties, that should be included to Exercise entity serialization. – Mikhail Prosalov Aug 04 '17 at 13:24
  • Would you please add this into the example so that I will accept the answer? Thanks. – StockBreak Aug 05 '17 at 14:19
  • Unfortunately I am receiving this error: `request.CRITICAL: Uncaught PHP Exception RuntimeException: "Can't pop from an empty datastructure" at /my_app/vendor/jms/serializer/src/JMS/Serializer/JsonSerializationVisitor.php line 142`. Seems to be related to nested serialization calls. Do you have any idea? Thanks. – StockBreak Aug 07 '17 at 09:32
  • Probably you do have a relation with Exercise entity in your ExerciseResult entity. There are two ways to fix it. Try set serialization rules using annotations in your ExerciseResult class (or implement a DTO class with a list of a fields to be exposed within a serialization process) to avoid adding Exercise relation to serialization result (in this case you'll get a loop here). OR use simple approach and implement toArray method for your ExerciseResult class, which will return an assoc array of fields to be serialized with Exercise entity. And just call toArray method in a listener. – Mikhail Prosalov Aug 07 '17 at 11:11
  • I already had `@Exclude` on the exercise property. After a while I found this post: https://github.com/schmittjoh/serializer/issues/319 and I solved by instantiating a new instance of the serializer to serialize the inner property instead of injecting it: `$serializer = SerializerBuilder::create()->build();`. It seems that serializing inside a `post_serialize` is not supported. – StockBreak Aug 07 '17 at 12:16
0

You can use expressions in VirtualProperties

For example:

/* @Serializer\VirtualProperty("profile_views", exp="service('app.profile_views_service').getViews(object)") */

In service you can inject any dependencies you need.

Vaidas Zilionis
  • 921
  • 8
  • 14