Я рассказывал про API *лионы раз за последний год. Множество отзывов и вопросов возникли в тот момент, когда я говорил о сериализации, как о "добавлении слоя представления вашим данным".

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


PHP-разработчики, часто рассматривают процесс серииализации, как использование функции serialize(). Да, это одна из форм сериализации, но ни она одна существует. Другой распространённый подход сериализации данных заключается в использовании функции json_encode(). На сегодняшний день современные фреймворки автоматически преобразовывают любой массив, возвращаемый из метода контроллера в JSON, это означает, что вам даже не нужно вызывать json_encode() самостоятельно.

Данная возможность достаточна удобна, но, если вы создаете HTTP API (AJAX/RESTful/Hypermedia), то вам нужно быть более точным с тем, что возвращаете.

Наиболее распространённый нарушением является следующее:
<?php
class PlaceController extends CoreController
{
    public function show($id)
    {
        return Place::find($id);
    }
}

Извините, за упрощенный код, но здесь мы получаем модель(возможно с использованием ORM) и возвращаем результат напрямую.
Это кажется довольно невинным действием, но ведет к целому ряду проблем.

Нет сокрытий


Вы добавляете все поля в результат вывода при выполнении API метода. Если это внутреннее API, то это может быть нормально, но если такая информация сопутствует везде браузеру, или как-то уходит за пределы мобильного устройства, то у вас действительно наступают не лучшие времена.

Банальный пример — пароли пользователей. Конечно, данные шифруются, но очевидно вы не хотите, чтобы они попали в чужие руки.
Чуть менее заметные вещи — это токены сброса паролей, утечка которых может привести к взлому пользователей.

Но проблема может быть более скрытой. В примере с Place, конфиденциальная информация может утечь из вашего бизнеса, когда вас просят добавить контактный email, предназначенный только для внутреннего использования. Если вы потратили месяцы на сбор этих мест и выстроили ряд уникальных контактов, вам не захочется, чтобы все данные просочились к потенциальным конкурентам.

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

К примеру, Fractal — созданная мною PHP библиотека сериализации, помогающая сериализовывать мои API-приложения — вот простой пример:

<?php
use League\Fractal\Manager;
use League\Fractal\Resource\Collection;
// Create a top level instance somewhere
$fractal = new Manager();
// Ask the ORM for 10 books
$books = Book::take(10)->get();
// Turn this collection 
$resource = new Collection($books, function(array $book) {
    return [
        'id'      => (int) $book->id,
        'title'   => $book->title,
        'year'    => (int) $book->yr,
        'author'  => [
        	'name'  => $book->author_name,
        	'email' => $book->author_email,
        ],
        'links'   => [
            [
                'rel' => 'self',
                'uri' => '/books/'.$book->id,
            ]
        ]
    ];
});


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

Такие средства существуют для многих языков. Я работал с библиотекой ActiveModel Serializer, которая устроена почти идентично.

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

Типы данных атрибутов


Многие языки программирования, включая PHP, довольно глупые, когда дело доходит до их драйверов связывания данных. Такие вещи, как MySQL и PostgreSQL имеют множество типов данных: integer, float, boolean, и прочие, но всё, что получает пользователь на выходе — обычная строка.

Вместо true и false вы увидите "1" и "0", или может быть даже "t" и "f". Числа с плавающей точкой на выходе представляют "-41.235" вместо -41.235.

Для слабо типизированного языка это может показаться не особенно важным, но строго типизированные языки будут падать, при таких изменениях. Особенно неприятно, когда строковое представление числа меняет свой тип на числовой во время выполнения математических операций в ORM акцессоре, в котором «1» + «2» = 3. Такое изменение может потенциально пройти ваши юнит-тесты, если последние достаточно cмутные, но оно "покалечит ваше IOS приложение до полусмерти".

ActiveRecord в Rails отслеживает какие типы полей должны присутствовать в соответствие с тем, как они добавлялись в схему посредством миграции, но, если эти изменения внесены посредством акцессора, или изменился тип данных в схеме — это также вызовет проблемы.

Используя сериализацию, как в примере выше, мы можете преобразовывать типы данных, для обеспечения нужного формат на выходе, и этот тип изменится только тогда, когда вы внесете изменения в сам сериализатор.

Переименование полей


Переименование полей в хранилище данных не нарушит ваш API. Если вас раздражает необходимость обновлять все ваши интеграционные тесты, то подумайте, как будет раздражать это разработчиков мобильных приложений, или других frontend-команд, которым необходимо обновить и развернуть новые приложения. Возможно вы даже не помните о lock-step развертывании. Если нет, тогда вы собираетесь получить нерабочие приложения для конечных пользователей, и даже если вы выкатите обновление для IOS, приложения все равно останутся нерабочими до тех пор, пока пользователи не обновят их вручную.

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

Несколько хранилищ данных


В большинстве из ORM-решений процесса сериализации, присутствует одно заблуждение — все ваши данные хранятся в одном месте. Что же произойдет, когда некоторые данные перетекут из SQL в Redis или куда-то еще?

Даже если вы не перемещаете часть данных из SQL в Redis, то можете разделить одну таблицу на две, или использовать сводные таблицы. В таком случае, большинство ORM-сериализаторов приземлятся на лицо, если вы попробуете выполнить это.

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

Версионирование сериализаторов


Ранее я версионировал сериализаторы для мажорных версий. V1 и V2 для FoodSerializer'а могли существовать обе, имея различные тесты, и отлично удовлетворяя многочисленные потребности API клиента.

Форматы сериализатора


Кое-что Fractal достиг не в полной мере — это мультиформатные «адаптеры», но стремится исправить это в версии 1.0. Довольно неплохо это было реализовано в Rails сообществе, и вы можете отправлять различные заголовки для получения совершенно другого формата данных.



В зависимости от mime-типа, посылаемого вами, вы указываете сериализатору какой формат данных нужен на выходе, не засоряя ваш код потенциально сложной логикой.

Решения


Я рассказал почему стоит использовать сериализацию, но не рассказал, как использовать. Для этого, взгляните на следующие решения:

Какую бы систему вы не выбрали, они имеют похожую идею. Если вы создаете какой-либо API, пожалуйста, воспользуйтесь этим.

Помните, что API это не только прокси для SQL-комманд, API необходимо планировать, тщательно продумывать и поддерживать, и простые изменения в вашем хранилище данных не должны сносить целую сеть приложений и сервисов.

Комментарии (3)


  1. AVil
    25.06.2015 11:57

    По поводу вашей библиотеки.
    Не хватает возможности исключения атрибутов, так как бывают модели с большим количеством атрибутов, да еще и отдающиеся фабрикой. Соотв. набор атрибутов возвращаемой модели может очень сильно отличаться.
    В таком случае гораздо проще было бы не перечислять атрибуты (а это может превратиться в ад из свитчей в описанном выше сценарии), а перечислить атрибуты, которые не надо включать в ответ.


    1. zelenin
      25.06.2015 13:26

      Библиотеки Phil Sturgeon — автора оригинальной статьи.


  1. Fesor
    26.06.2015 02:03

    jmsserializer можно использовать отдельно от symfony если что. Есть еще symfony/serializer