objects namespaces and application architecture
Serialization
Serialization converts values or objects into a format that can be stored or sent somewhere else. Deserialization converts that stored format back into PHP values or objects.
In PHP work, serialization appears in sessions, caches, queues, cookies, API payloads, logs, database columns, and background jobs. The format might be PHP's native serialize(), JSON, XML, MessagePack, or a framework-specific payload.
The practical skill is choosing the right format for the boundary and avoiding unsafe deserialization.
Native PHP Serialization
serialize() turns a PHP value into a PHP-specific string. unserialize() restores it.
<?php
declare(strict_types=1);
$payload = serialize([
'user_id' => 10,
'email' => 'ada@example.com',
]);
echo $payload . PHP_EOL;
$restored = unserialize($payload, ['allowed_classes' => false]);
if (!is_array($restored)) {
throw new RuntimeException('Expected an array.');
}
echo $restored['email'] . PHP_EOL;
Native serialization can preserve PHP-specific types, but it is not a good public data format. Other languages do not naturally understand it, and class changes can break old serialized objects.
Do not call unserialize() on untrusted input. Object deserialization can trigger magic methods and has been the source of serious security vulnerabilities in PHP applications.
JSON Serialization
JSON is usually better for APIs, logs, webhooks, and data shared with other systems.
<?php
declare(strict_types=1);
$json = json_encode([
'user_id' => 10,
'email' => 'ada@example.com',
], JSON_THROW_ON_ERROR);
echo $json . PHP_EOL;
$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new RuntimeException('Expected decoded JSON to be an array.');
}
echo $data['email'] . PHP_EOL;
// Prints:
// {"user_id":10,"email":"ada@example.com"}
// ada@example.com
Use JSON_THROW_ON_ERROR so invalid JSON fails clearly instead of returning null silently.
JsonSerializable
Objects can control their JSON representation by implementing JsonSerializable.
<?php
declare(strict_types=1);
final readonly class ApiUser implements JsonSerializable
{
public function __construct(
private int $id,
private string $email,
private string $passwordHash,
) {
}
/** @return array{id: int, email: string} */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'email' => $this->email,
];
}
}
echo json_encode(
new ApiUser(10, 'ada@example.com', 'hash-value'),
JSON_THROW_ON_ERROR
) . PHP_EOL;
// Prints:
// {"id":10,"email":"ada@example.com"}
This is useful when the object has private properties or sensitive fields that should not be exposed.
__serialize() And __unserialize()
Objects can control native PHP serialization with __serialize() and __unserialize().
<?php
declare(strict_types=1);
final class RememberedUser
{
public function __construct(
private int $id,
private string $email,
private string $temporaryToken,
) {
}
/** @return array{id: int, email: string} */
public function __serialize(): array
{
return [
'id' => $this->id,
'email' => $this->email,
];
}
/** @param array{id: int, email: string} $data */
public function __unserialize(array $data): void
{
$this->id = $data['id'];
$this->email = $data['email'];
$this->temporaryToken = '';
}
public function email(): string
{
return $this->email;
}
}
$serialized = serialize(new RememberedUser(10, 'ada@example.com', 'secret'));
$restored = unserialize($serialized, ['allowed_classes' => [RememberedUser::class]]);
if (!$restored instanceof RememberedUser) {
throw new RuntimeException('Unexpected serialized value.');
}
echo $restored->email() . PHP_EOL;
// Prints:
// ada@example.com
This can be useful for sessions or cache entries, but keep long-term compatibility in mind. Renaming the class or changing its structure can break old serialized payloads.
Serialization Boundaries
Before serializing data, ask where it is going.
For an API response, JSON with clear field names is usually right. For an internal queue job, a JSON payload containing IDs may be better than serializing a whole object graph. For sessions, small scalar values are safer than large domain objects. For logs, avoid secrets and personally sensitive data unless there is a clear policy.
Prefer serializing data transfer shapes, not live service objects. Do not serialize database connections, HTTP clients, loggers, closures, or objects with external resources.
What To Watch For
Common serialization problems include:
- silently ignoring
json_encode()orjson_decode()failures - exposing passwords, tokens, or internal fields in API responses
- storing serialized PHP objects for years and then changing class names
- unserializing untrusted input
- putting large object graphs into sessions or queues
- assuming a decoded JSON array has the expected shape without checking
Serialization is a boundary. Treat it like one: validate what comes in and deliberately shape what goes out.
What You Should Be Able To Do
After this lesson, you should be able to use JSON encoding and decoding safely, explain when native PHP serialization is appropriate, implement JsonSerializable, and understand why unserialize() on untrusted input is dangerous.
For junior work, the practical skill is not memorising every serialization format. It is being careful about what data crosses a boundary and how that data will be restored later.
Practice
Practice: Create A Safe JSON Response
Create a small PHP example for serializing a user into JSON without exposing sensitive fields.
Task
Build an ApiUser class that:
- stores an ID, email, and password hash
- implements
JsonSerializable - exposes only the ID and email in JSON
Then encode it with json_encode() using JSON_THROW_ON_ERROR.
Use strict types. Keep the expected output in the PHP code block as printed lines or comments.
Check Your Work
Confirm:
- the JSON includes the ID and email
- the JSON does not include the password hash
- encoding errors would throw instead of failing silently
Afterward, explain why this is safer than serializing every object property automatically.
Show solution
This solution makes the JSON response shape explicit and keeps the password hash out of the serialized output.
<?php
declare(strict_types=1);
final readonly class ApiUser implements JsonSerializable
{
public function __construct(
private int $id,
private string $email,
private string $passwordHash,
) {
}
/** @return array{id: int, email: string} */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'email' => $this->email,
];
}
public function passwordHashIsExposedIn(string $json): bool
{
return str_contains($json, $this->passwordHash);
}
}
$user = new ApiUser(10, 'ada@example.com', 'hashed-password-value');
$json = json_encode($user, JSON_THROW_ON_ERROR);
echo $json . PHP_EOL;
echo $user->passwordHashIsExposedIn($json) ? 'leaked' : 'hidden';
echo PHP_EOL;
// Prints:
// {"id":10,"email":"ada@example.com"}
// hidden
This is safer than automatically serializing every property because the public response shape is deliberate. Sensitive fields stay private unless the class explicitly includes them.