Если вы когда-либо создавали сценарий экспорта или API, то вам наверняка приходилось форматировать свой контент и иметь дело с сериализацией.
В Symfony этой задачей зачастую занимается JMS Serializer (так указано в документации Symfony).
Но попользовавшись JMS Serializer в нескольких проектах, я остался им не совсем доволен. Я столкнулся с мелкими недочетами, преимущественно с предположениями, которые не соответствуют моим требованиям и которые невозможно легко переопределить или перезадать. На мой взгляд, один этот факт существенно снижает эффективность JMS Serializer.
Возможно, JMS Serializer подойдет для крупных бекендов и тех API, в которых вы хотите сериализовать объекты «автоматически».
Но если вы работаете над бизнес-логикой для небольших/средних проектов (чем я преимущественно и занимаюсь), вам нужны более гибкие варианты.
Но знаете что?
У Symfony есть отличный компонент сериализации!
Symfony уже решила проблему сериализации контента с Serializer Component.
Пусть он не такой готовый к использованию, как JMS Serializer, но зато он расширяемый и гибкий.
Обратите внимание: В компоненте сериализации Symfony сериализатор состоит из двух частей:
- Нормализатор: отвечает за преобразование исходного объекта в массив (нормализировать/денормализировать).
- Кодировщик: отвечает за преобразование нормализированных данных в отформатированную строку (закодировать/декодировать).
Чтобы сериализатор смог выполнить больше задач сериализации, вы можете оснастить его несколькими нормализаторами и кодировщиками.
Если вы не знакомы с этим компонентом, то перед тем как двигаться дальше, я рекомендую вам почитать документацию.
Ваша бизнес-логика определяется нормализатором
Компонент Serializer имеет несколько кодировщиков (JSON и XML), но вы без труда можете написать кодировщик для любого нужного вам формата: CSV, XML,…
Но суть задачи сериализации заключается в том, чтобы преобразовать ваш объект в массив (также это называется нормализацией). Именно это вы и делаете, когда при использовании JMS пишете аннотации с указанием того, какое свойство нужно включить и как.
Это и есть самое главное, и именно в этом направлении следует прилагать свое время и усилия.
Если вы хотите сериализировать какой-либо объект определенным образом, используйте нормализатор, который поддерживает именно эту модель!
Чтобы создать пользовательский нормализатор, вам необходимо реализовать NormalizerInterface
, который описывает два метода:
supportsNormalization
: Отвечает на вопрос «Можно ли нормализовать этот объект?»normalize
: Осуществляет преобразование объекта в массив.
Вот пример:
<?php
namespace Acme\Serializer\Normalizer;
use Acme\Model\User;
use Acme\Model\Group;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* User normalizer
*/
class UserNormalizer implements NormalizerInterface
{
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'id' => $object->getId(),
'name' => $object->getName(),
'groups' => array_map(
function (Group $group) {
return $group->getId();
},
$object->getGroups()
)
];
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}
Примечание: Вы можете добавить сюда логики/сложности, вы отделили модель от сериализации модели.
Результат нормализации будет выглядеть следующим образом:
<?php
[
'id' => 1,
'name' => 'Foo Bar',
'groups' => [1, 2]
]
Работа с ассоциациями объекта
В процессе нормализации объекта вы можете обнаружить его связи с другими объектами, которые не поддерживаются нормализатором. В этом случае вам поможет SerializerAwareNormalizer
: Когда ваш нормализатор расширит SerializerAwareNormalizer
, то в качестве зависимости он получит родительский сериализатор. Так что вы можете поручить нормализацию другого объекта сериализатору (у которого могут иметься другие нормализаторы для этого объекта).
Давайте обновим наш предыдущий пример:
<?php
namespace Acme\Serializer\Normalizer;
// ...
use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer;
/**
* User normalizer
*/
class UserNormalizer extends SerializerAwareNormalizer implements NormalizerInterface
{
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
// ...
'groups' => array_map(
function ($object) use ($format, $context) {
return $this->serializer->normalize($object, $format, $context);
},
$object->getGroups()
),
];
}
}
Теперь вам лишь нужно написать нормализатор, который поддерживает объекты Group
!
<?php
// ...
/**
* Group normalizer
*/
class GroupNormalizer extends SerializerAwareNormalizer implements NormalizerInterface
{
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'id' => $object->getId(),
'name' => $object->getName(),
];
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof Group;
}
}
Результат нормализации:
<?php
[
'id' => 1,
'firstname' => 'Foo',
'lastname' => 'Bar',
'groups' => [
[
'id' => 1,
'name' => 'FooFighters'
],
[
'id' => 2,
'name' => 'BarFighters'
],
],
]
Примечание: В отношении метода
supportsNormalization
можно смело сказать, что вы работаете с конкретным интерфейсом, а не с объектом. Так вы получаете нормализатор, работающий со всеми моделями, которые демонстрируют определенный тип поведения.
Контекст
Компонент Serializer
предлагает переменную $context
, которая присутствует на протяжении всего процесса сериализации.
Вы можете использовать ее для хранения любой информации, которая понадобится вашему нормализатору, управляя его работой.
<?php
namespace Acme\Serializer\Normalizer;
// ...
use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer;
/**
* User normalizer
*/
class UserNormalizer extends SerializerAwareNormalizer implements NormalizerInterface
{
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
// ...
'groups' => array_map(
function ($object) use ($format, $context) {
if ($context['include_relations']) {
return $this->serializer->normalize($object, $format, $context);
} else {
return $object->getId();
}
},
$object->getGroups()
),
];
}
}
Видите, как наш сериализатор становится все более гибким и функциональным?
Вот еще несколько пользовательских нормализаторов, которые я написал для API REST:
- Doctrine’s Collection: похоже на то, что мы делали с группами в примере выше.
- DateTime: единственный класс в моем приложении, отвечающий за форматирование Дат API.
- Form Error: возвращает простой массив с именами полей в качестве ключей и сообщениями об ошибке в качестве значений.
Также вы можете расширить ObjectNormalizer
, идущий вместе с компонентом Symfony Serializer. Он обрабатывает все свойства объекта, который необходимо сериализировать, и передает все нескалярные величины обратно сериализатору (то есть он может работать с вашими пользовательскими нормализаторами!). Но поскольку он не делает предположения о том, как обрабатывать циклические зависимости, над ним нужно немного попотеть. Но об этом в другой статье.
Сериализатор как сервис
Когда у вас есть все необходимые нормализаторы, следует объявить их сервисами. Предоставляя этим нормализаторам доступ к возможностям сервиса, вы добавляете зависимости, которые поставляют дополнительную информацию в процессе нормализации (база данных, веб-сервис, файловая система и т.д.).
Для этого объявите сервис для каждого кодировщика, который вы будете использовать:
services:
# JSON Encoder
acme.encoder.json:
class: 'Symfony\Component\Serializer\Encoder\JsonEncoder'
# XML Encoder
acme.encoder.xml:
class: 'Symfony\Component\Serializer\Encoder\XmlEncoder'
Объявите сервисами свои пользовательские нормализаторы:
services:
# User Normalizer
acme.normalizer.user:
class: 'Acme\Serializer\Normalizer\UserNormalizer'
# Group Normalizer
acme.normalizer.group:
class: 'Acme\Serializer\Normalizer\GroupNormalizer'
Наконец, составьте столько сериализаторов с разными нормализаторами и кодировщиками, сколько вам понадобится:
services:
# Serializer
acme.serializer.default:
class: 'Symfony\Component\Serializer\Serializer'
arguments:
0:
- '@acme.normalizer.user'
- '@acme.normalizer.group'
- '@serializer.normalizer.object'
1:
- '@acme.encoder.json'
- '@acme.encoder.xml'
Примечание: Если вы включили сервисы сериализатора, как сделал в данном случае я, вы можете использовать сервис serializer.normalizer.object в качестве резервного нормализатора для всех объектов, для работы с которыми вы не использовали пользовательский нормализатор.
Преимущества:
Структурируя таким образом свой сериализатор, вы получаете модульную и гибкую систему:
Гибкость: при создании своих собственных нормализаторов вам не нужно соответствовать структуре, навязываемой библиотекой. Просто напишите код, необходимый для вашего проекта.
Тестируемость: нормализаторы являются тестируемымы классами PHP; вы можете осуществить модульное тестирование результата нормализации.
Разделение задач: может получиться так, что разным частям вашего приложения нужно будет сериализовать один и тот же объект разными способами. Знать, как это сделать – это их задача, а не задача самого объекта: а это значит, что вы можете забыть о сложных наборах групп в аннотациях объекта.
Эффективность работы: Исходя из собственного опыта, могу сказать, что Symfony Serializer с пользовательскими нормализаторами работает в 3 раза быстрее, чем JMS Serializer.