JMS VirtualProperty с аргументом и пользовательским слушателем/подписчиком

Я пытаюсь сериализовать свойство, которое является критериями доктрины:

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

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

Я не могу использовать @VirtualProperty, потому что ему нужен аргумент, поэтому я реализовал настраиваемый подписчик для одного из моих типов после этого поста:

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 {}
        );
    }
}

К сожалению, свойство user_results всегда пусто!
Я просмотрел исходный код сериализатора и обнаружил, что:

/**
 * 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;
}

Обратите внимание: Он больше не должен содержать никаких объектов.

Как я могу это решить?


person StockBreak    schedule 01.08.2017    source источник


Ответы (1)


Попробуйте использовать событие serializer.post_serialize вместо события serializer.pre_serialize. Также имя вашего виртуального свойства (user_results) должно отличаться от любых существующих сериализуемых полей.

Второй аргумент метода setData должен быть либо обычным скаляром, либо массивом. Он не должен содержать никаких объектов. Я предлагаю внедрить сериализатор JMS в ваш класс слушателя, чтобы сериализовать массив ваших объектов в массив скаляров.

<?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))
        );
    }
}
person Mikhail Prosalov    schedule 01.08.2017
comment
Я обновил свой ответ, указав правильное событие post_serialize, но оно не работает. Пожалуйста, смотрите мой последний комментарий, выделенный жирным шрифтом. - person StockBreak; 02.08.2017
comment
Обновил мой ответ. Переменная $results не должна содержать никаких объектов. Пожалуйста, сериализуйте его перед вызовом метода setData. - person Mikhail Prosalov; 02.08.2017
comment
Проблема в том, что getUserResults возвращает список ExerciseResult. Если я сериализую его перед вызовом setData, не появится ли он в виде сериализованной строки (закодированной)? - person StockBreak; 04.08.2017
comment
Вы должны сериализовать его в массив. Вы можете внедрить сериализатор jms в этот класс прослушивателя и использовать его для сериализации объектов ExerciseResult в массив с помощью метода toArray. Или вы можете реализовать метод toArray в вашем объекте ExerciseResult для возврата массива с важными свойствами, которые должны быть включены в сериализацию объекта Exercise. - person Mikhail Prosalov; 04.08.2017
comment
Не могли бы вы добавить это в пример, чтобы я принял ответ? Спасибо. - person StockBreak; 05.08.2017
comment
К сожалению, я получаю эту ошибку: 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. Кажется, это связано с вложенными вызовами сериализации. Есть ли у вас какие-либо идеи? Спасибо. - person StockBreak; 07.08.2017
comment
Возможно, у вас есть связь с сущностью «Упражнение» в сущности «Упражнениерезультат». Есть два способа это исправить. Попробуйте установить правила сериализации с помощью аннотаций в вашем классе ExerciseResult (или внедрите класс DTO со списком полей, которые будут отображаться в процессе сериализации), чтобы избежать добавления отношения «Упражнение» к результату сериализации (в этом случае вы получите здесь цикл) . ИЛИ используйте простой подход и реализуйте метод toArray для вашего класса ExerciseResult, который будет возвращать ассоциированный массив полей, которые будут сериализованы с сущностью Exercise. И просто вызовите метод toArray в слушателе. - person Mikhail Prosalov; 07.08.2017
comment
У меня уже было @Exclude в свойстве упражнений. Через некоторое время я нашел этот пост: github.com/schmittjoh/serializer/issues/319 и я решил, создав новый экземпляр сериализатора для сериализации внутреннего свойства вместо его внедрения: $serializer = SerializerBuilder::create()->build();. Кажется, что сериализация внутри post_serialize не поддерживается. - person StockBreak; 07.08.2017