Избегание рекурсии с объектами Doctrine и JMSserializer

Я создаю REST API, используя Symfony2, Doctrine, FOSRestBundle и JMSSerializer.

Проблема, с которой я столкнулась, - это сериализация моих объектов, сериализатор тянет любые связанные объекты. Например, для задачи, которая является частью истории, которая является частью доски, поэтому при сериализации задачи я получаю вывод, который включает в себя историю, которая включает в себя плату, которая затем включает в себя все другие истории на доске.

Есть ли простой способ ограничить это, и просто включите foreignIds вместо этого?

Ответы

Ответ 1

Проверьте файл Serializer/Handler/DoctrineProxyHandler.php на JMSSerializerBundle. Теперь, если вы прокомментируете эту строку:

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

Это прекратит ленивую загрузку ваших объектов. Если это то, что вы ищете, тогда просто идите, создайте свой собственный обработчик, где вы не будете ленивой загрузкой.

Если это неверно, я рекомендую вам настроить свои объекты перед отправкой их на JMSSerializerBundle по своему вкусу. Например, в любых связанных объектах я хочу ID, в то время как в других мне нужно собственное имя столбца, например код или имя, или что-то еще.

Я просто создаю копию моего объекта entity, а затем начинаю получать поля, необходимые для отношений. Затем я сериализую эту копию. JMSSerializerBundle не будет ленивой загрузкой, потому что я уже предоставил правильные поля.

Ответ 2

Использовать политику исключения JMS.

Пример использования аннотаций в категории, где вы не хотите включать дочерние элементы и связанные с продуктом объекты, которые должны быть включены:

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

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

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

}

Дополнительную информацию смотрите в JMSSerializer docs.

EDIT:

Например, вы можете использовать частичное ключевое слово, чтобы выбрать только те данные, которые вам нужны. Хотя я не мог, для жизни меня, отключите загрузку полных связанных объектов (два уровня вниз), если я передаю объект объекта в сериализатор (даже при отключении нагрузки в DoctrineProxyHandler) но если я использую массив, то он не использует доктрину ленивой загрузки, хотя прокси (как и ожидалось).

Пример использования ваших объектов-примеров:

$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();

Таким образом вы получите что-то вроде:

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

}
]

P.S. Я действительно заинтригован этой "проблемой". В любом случае я увижу решение о том, как сериализовать объект объекта без использования результата массива.

Ответ 3

Просто обновление в последней версии JMSSerializer, место, которое вы должны посмотреть, это

JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber

вместо

Serializer\Handler\DoctrineProxyHandler

Чтобы переопределить поведение ленивой загрузки по умолчанию, нужно определить его собственный подписчик событий.

В вашем приложении /config.yuml добавьте это:

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

вы можете скопировать класс из JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber в свой \Bundle\Event\DoctrineProxySubscriber и прокомментировать $object → __ load(); Линия

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

Обновление. В итоге я написал собственную упрощенную версию инструмента сериализации: https://github.com/dlin-me/array-converter-bundle

Ответ 4

Здесь функция для выбора идентификаторов взаимно-однозначных или связанных друг с другом объектов в общих чертах без использования объединений.

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

Ответ 5

Вот класс, который предотвращает ленивую загрузку одной или многих ассоциаций, которые могут использоваться как 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/info/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;
    }
}

Пример использования:

$serializationContext->addExclusionStrategy(
     new OnlyLoadedAssociationsExclusionStrategy()
);