Блог веб-разработчика v 1.0.0
Symfony2, AngularJS, React, Gulp, PhpStorm и много других страшных слов

(Де)сериализация сущностей Doctrine2 в приложении Symfony2

10 лет назад
10953 просмотра
Doctrine2 PHP PHP Frameworks Symfony2 XML

(Де)сериализация сущностей Doctrine2 в Symfony2Иногда необходимо преобразовать сущности Doctrine2 в JSON объект или XML структуру. Конечно, вручную это делать ужасно неудобно и долго. Поэтому, вспоминая, что "все давно придумали за вас", будем использовать сторонние средства для решения поставленной задачи.

JMSSerializerBundle - этот бандл предоставляет сервис для доступа к библиотеке serializer, которая умеет (де)сериализовать практически всё, в том числе и сущности Doctrine2.

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

Установка JMSSerializerBundle

Установка JMSSerializerBundle довольно проста. Добавляем зависимость в composer и регистрируем бандл в нашем Symfony2 приложении:

composer require jms/serializer-bundle
// AppKernel::registerBundles()
$bundles = array(
     // ...
     new JMS\SerializerBundle\JMSSerializerBundle(),
     // ...
);

Бандл не требует настройки, однако, если вам очень хочется, то понастраивать его все же можно прочитав документацию.

Теперь нам доступен сервис jms_serializer, используемый для (де)сериализации данных в вашем приложении. Пример:

$serializer = $container->get('jms_serializer');
$serializer->serialize($data, $format);
$data = $serializer->deserialize($inputStr, $typeName, $format);

В качестве $data может выступать любое значение, будь то массив, объект или их комбинация.

Иногда при дописывании класса сущности сериализатор может как-бы "не видеть" внесенных изменений. В этом случае попробуйте очистить APC кеш.

(Де)сериализация сущностей Doctrine2 в XML

Сущность - по сути просто объект. Serializer даже не знает, что она как-то связана с Doctrine2, поэтому работает с практически любым объектом. Скалярные типы и массивы не требуют описания, однако поля объектов все же необходимо как-то обозначить. Если в будущем вам не нужно десериализовывать объект, то можно обойтись минимальным описанием, например так:

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;


/**
 * @ORM\Entity
 * @JMS\ExclusionPolicy("all")
 */
class MyClass
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     *
     * @JMS\Expose
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     *
     * @JMS\Expose
     */
    private $title;

    /**
     * @ORM\Column(type="datetime")
     */
    private $created;
}

Здесь с помощью аннотации ExclusionPolicy мы запретили сериализовать все поля, а затем с помощью Expose разрешили нужные нам id и title. При этом, заметьте, что поле created не будет сериализовано, т.к. отсутствует аннотация Expose. Сериализация в XML происходит следующим образом:

// В вашем контроллере
$this->get('jms_serializer')->serialize(new MyClass(), 'xml');

В качестве формата можно указывать  xml, json или yml.

Если нам в будущем необходимо будет десериализовать результат обратно в объект, то каждому полю требуется указывать тип (например, через аннотацию Type):

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;


/**
 * @ORM\Entity
 * @JMS\ExclusionPolicy("all")
 */
class MyClass
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     *
     * @JMS\Expose
     * @JMS\Type("integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     *
     * @JMS\Expose
     * @JMS\Type("string")
     */
    private $title;
}

В таком случае при десериализации библиотека будет знать к какому типу приводить каждое поле. Десериализация в объект происходит следующим образом:

// В вашем контроллере
$this->get('jms_serializer')->deserialize($xmlString, 'MyClass', 'xml');

Обратите внимание, что необходимо указывать класс объекта вторым параметром. Если у вас массив или строка, то там будет array и string соответственно.

При использовании связей в сущности в типе данных необходимо указывать результирующий класс:

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;


/**
 * @ORM\Entity
 * @JMS\ExclusionPolicy("all")
 */
class MyClass
{
    /**
     * @JMS\Expose
     * @JMS\Type("AnotherClass")
     * 
     * @ORM\ManyToOne(targetEntity="AnotherClass")
     * @ORM\JoinColumn(name="ac_id", referencedColumnName="id", onDelete="SET NULL")
     */
    private $another;

    /**
     * @JMS\Expose
     * @JMS\Type("ArrayCollection<AnotherClass>")
     * 
     * @ORM\ManyToMany(targetEntity="AnotherClass")
     * @ORM\JoinTable(name="table", joinColumns={@ORM\JoinColumn(name="mc_id", referencedColumnName="id", onDelete="CASCADE")}, inverseJoinColumns={@ORM\JoinColumn(name="ac_id", referencedColumnName="id", unique=true, onDelete="CASCADE")})
     */
    private $anothermany = null;

    public function __construct()
    {
        $this->anothermany = new \Doctrine\Common\Collections\ArrayCollection();
    }
}

Для связи многие к одному мы указали в качестве типа класс связанной сущности. Для типа многие ко многим мы указали ArrayCollection из элементов привязанной сущности.

При десериализации таких объектов автоматически воссоздадутся все связи сущности.

Известные хитрости

Допустим, вы делаете импорт в ваш проект из XML файла. Сущности, описанные в файле имеют связи, например, с районами города (ManyToOne). Логично, что районы не должны каждый раз создаваться заново, а должны использоваться уже существующие записи в БД. Чтобы каждый раз не создавать новые записи после flush() десериализованной сущности, вместо $entityManager->persist($entity) необходимо выполнить $entityManager->merge($entity). Однако при мерже доктрина сбросит указанные вручную после десериализации связи. Это известный баг о котором нужно просто помнить и после мержа вручную переназначать связи. Пример:

<?xml version="1.0" encoding="UTF-8"?>
<result>
<entry>
<title><![CDATA[Тестовый объект]]></title>
<district>
<id>1</id>
<title><![CDATA[Западный Округ]]></title>
</district>
</entry>
</result>

Как видим, у поля district уже задан ID. Не смотря на это, доктрина все равно создаст новую запись в БД если использовать persist(), а не merge().

Что еще почитать