Понимание сериализации в PHP
Сериализованное представление данных может быть достигнуто с помощью различных форматов. Очень распространён 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 для целого числа в спецификаторе типа стоит 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!