Понимание сериализации
Сериализация — это процесс, в котором принимается часть данных и создаётся хранимое или переносимое представление этих данных.
Сериализованное представление данных может быть достигнуто с помощью различных форматов. Очень распространён JSON, поскольку большинство языков в той или иной форме кодируют и декодируют JSON. Можно также использовать XML, YAML или даже просто строку байтов.
Вы, вероятно, знакомы с функцией PHP serialize(). Эта функция принимает одно значение и возвращает его сериализованную версию в виде строки.
Уникальность сериализации в PHP заключается в том, что она фактически использует специальный формат сериализации для представления различных типов данных, включая массивы и объекты.
Каждый тип данных, который PHP может сериализовать, имеет своё собственное представление и обозначение при сериализации. Давайте разберём их и посмотрим, что они собой представляют.
Логические значения
Логические значения очень просты:
b:0;
b:1;
Спецификатором типа для логических значений является b. Затем следует двоеточие и целочисленное представление самого логического значения, так что false становится 0, а true — 1.
Null
Сериализация null приводит к следующему:
N;
Null не содержит никаких дополнительных данных, поэтому он представляется одним символом N.
Целые числа и числа с плавающей запятой
Сериализация значения 100 с помощью функции serialize() возвращает следующую строку:
i:100;
Сериализованные целые числа представляются символом i, за которым следует двоеточие и значение целого числа.
Значение целого числа всегда сериализуется в «десятичном» виде (основание 10). Сериализованная версия не содержит информации об исходном формате значения (шестнадцатеричный, восьмеричный, двоичный и т.д.).
Сериализация значения 100.5 имеет очень похожий формат:
d:100.5;
Единственное отличие заключается в том, что вместо i для целого числа в спецификаторе типа стоит
Строки
Сериализованные строки несут в себе некоторую дополнительную информацию. Приведённый ниже код является результатом сериализации 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.