Иногда необходимо преобразовать сущности Doctrine2 в JSON объект или XML структуру. Конечно, вручную это делать ужасно неудобно и долго. Поэтому, вспоминая, что "все давно придумали за вас", будем использовать сторонние средства для решения поставленной задачи.
JMSSerializerBundle - этот бандл предоставляет сервис для доступа к библиотеке serializer, которая умеет (де)сериализовать практически всё, в том числе и сущности Doctrine2.
В статье будут использованы аннотации к класу для настройки сериализации, поэтому предполагается, что вы имеете соответствующие навыки.
Установка 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 кеш.
Сущность - по сути просто объект. 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().