Понимание сериализации в PHP

Источник: «Understanding Serialisation in PHP»
Сериализация — это процесс, в котором принимается часть данных и создаётся хранимое или переносимое представление этих данных.

Сериализованное представление данных может быть достигнуто с помощью различных форматов. Очень распространён JSON, поскольку большинство языков в той или иной форме кодируют и декодируют JSON. Можно также использовать XML, YAML или даже просто строку байтов.

Вы, вероятно, знакомы с функцией PHP serialize(). Эта функция принимает одно значение и возвращает его сериализованную версию в виде строки.

Уникальность сериализации в PHP заключается в том, что она фактически использует специальный формат сериализации для представления различных типов данных, включая массивы и объекты.

Каждый тип данных, который PHP может сериализовать, имеет своё собственное представление и обозначение при сериализации. Давайте разберём их и посмотрим, что они собой представляют.

Логические значения

Логические значения очень просты.

b:0;
b:1;

Спецификатором типа для логических значений является b. Затем следует двоеточие и целочисленное представление самого логического значения, так что false становится 0, а true1.

Null

Сериализация null приводит к следующему.

N;

Null не содержит никаких дополнительных данных, поэтому он представляется одним символом N.

Целые числа и числа с плавающей запятой

Сериализация значения 100 с помощью функции serialize() возвращает следующую строку.

i:100;

Сериализованные целые числа представляются символом i, за которым следует двоеточие и значение целого числа.

Примечание. Значение целого числа всегда сериализуется в "десятичном" виде (основание 10). Сериализованная версия не содержит информации об исходном формате значения (шестнадцатеричный, восьмеричный, двоичный и т.д.).

Сериализация значения 100.5 имеет очень похожий формат.

d:100.5;

Единственное отличие заключается в том, что вместо i для целого числа в спецификаторе типа стоит d для "double".

Строки

Сериализованные строки несут в себе некоторую дополнительную информацию. Приведённый ниже код является результатом сериализации Hello, world.

s:12:"Hello, world";

В качестве спецификатора типа используется s. Затем следует двоеточие и результат strlen($value). Затем следует ещё одно двоеточие и исходное значение, заключённое в двойные кавычки.

s:[strlen(value)]:"[value]"

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

Это также помогает в языках более низкого уровня, где строки могут храниться в виде массива байтов. Если вы знаете, сколько байт занимает строка, вы можете выделить нужный объем и избежать возможных проблем с памятью.

Массивы

Сериализация массивов немного сложнее, поскольку массив PHP имеет ключи и значения. Сначала сериализуем пустой массив [].

a:0:{}

Спецификатором типа массива является a. Вторым компонентом в сериализованных данных является длина массива. Третий компонент — место размещения ключа и значений массива.

a:[count(value)]:{...values}

Чтобы увидеть, как происходит сериализация значений, мы можем сериализовать простой массив с тремя значениями [1, 2, 3].

a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}

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

Информация. Помните, что хотя мы не указали в массиве никаких ключей, PHP автоматически проиндексирует значения, начиная с 0.

Общее формирование значений выглядит следующим образом.

{key;value;key;value;key;value}

Развёртывание исходного массива в более явный эквивалент с ключом.

[
0 => 1,
1 => 2,
2 => 3,
]

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

{i:0;i:1;i:1;i:2;i:2;i:3;}
^0 ^1 ^1 ^2 ^2 ^3

Если немного изменить массив и вместо ключей использовать строки.

[
'a' => 1,
'b' => 2,
'c' => 3,
]
a:3:{s:1:"a";i:1;s:1:"b";i:2;s:1:"c";i:3;}
^a ^1 ^b ^2 ^c ^3

Также можно сериализовать вложенные массивы, что полезно для более сложных структур.

a:1:{i:0;a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}}

Объекты

Наиболее сложный формат сериализации можно наблюдать при сериализации объектов.

Возьмём простой класс Nothing, создадим его и сериализуем.

class Nothing
{
// ...
}

serialise(new Nothing);
O:7:"Nothing":0:{}

Спецификатором типа для сериализуемых объектов является O. Далее следует длина имени класса и само имя класса, заключённое в двойные кавычки.

Число, следующее за именем класса, — это количество свойств, которыми обладает объект. Далее в фигурных скобках располагаются сериализованные свойства в формате, аналогичном массивам.

На примере нового класса User, имеющего 2 свойства — $name и $age, мы можем увидеть, как происходит сериализация значений свойств.

class User
{
public function __construct(
public $name,
public $age,
) {}
}

serialize(new User(
name: "Ryan",
age: 23
));
O:6:"User":2:{s:4:"name";s:4:"Ryan";s:3:"age";i:23;}

Объект User имеет 2 свойства, поэтому имя каждого свойства сериализуется в виде строки, а после него идёт сериализованное значение.

Преобразование этого формата в упрощённую грамматику может выглядеть следующим образом.

O:[strlen(value::class)]:"[value::class]":[count(properties)]:{...property}

property ::= [name];[value]

Непубличные свойства

Класс User имеет только публичные свойства, но функция serialize() может также сериализовать защищённые и приватные свойства.

Начиная с защищённых свойств, напишем новый класс SensitiveStuff, имеющий одно защищённое свойство $password.

class SensitiveStuff
{
public function __construct(
protected $password,
) {}
}

serialize(new SensitiveStuff(
password: 'password123'
));
O:14:"SensitiveStuff":1:{s:11:"\0*\0password";s:11:"password123";}

Все выглядит очень похоже, но можно заметить, что в месте имени свойства есть несколько дополнительных символов. Вместо того чтобы имя свойства сериализовалось в s:8: "password", здесь присутствуют три дополнительных символа: \0*\0.

Символ \0 известен как нулевой терминатор или нулевой байт. Символ * (звёздочка/астериск) обозначает это свойство как защищённое.

Если мы изменим видимость свойства на private и выполним повторную сериализацию, то получим несколько иное значение.

O:14:"SensitiveStuff":1:{s:24:"\0SensitiveStuff\0password";s:11:"password123";}

На этот раз имя свойства снабжено префиксом из нулевого байта, имени класса объекта и ещё одного нулевого байта. Данный шаблон представляет собой приватное свойство.

Ресурсы

Сериализация значений resource в PHP невозможна. При попытке сериализации объекта с ресурсом, хранящимся в свойстве, он будет приведён к целому числу и сериализован как 0.

Заключение

Спасибо за чтение! Надеюсь, это дало вам некоторое представление о том, как сериализуются различные типы значений в PHP.

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

Дополнительные материалы

Предыдущая Статья

Ленивая загрузка в JavaScript

Следующая Статья

Пять практических примеров использования регулярных выражений