Contents
- 1 Php serialization explained: how we freeze time, ship objects, and (sometimes) shoot ourselves in the foot
- 2 What php serialization actually is
- 3 The anatomy of a serialized value
- 4 Why php serialization refuses to die
- 5 Serialization vs json vs igbinary vs others
- 6 The dark side: php object injection and security
- 7 When serialization starts to hurt: refactors and migrations
- 8 Sleep, wakeup, and magic around objects
- 9 Php serialization in real projects: where it quietly hides
- 10 Stable boundaries: from raw serialization to explicit contracts
- 11 When to reach for php serialization today
- 12 Hiring and being hired: what serialization reveals about a developer
- 13 A few practical patterns you can use tomorrow
- 14 The quiet art of freezing time
Php serialization explained: how we freeze time, ship objects, and (sometimes) shoot ourselves in the foot
There’s a particular kind of silence in the office at 10:47 PM.
You know the one.
The lights are half-off, everyone’s Slack status has quietly flipped to “away”, and you’re still there, staring at a unserialize() warning in a log from a production server that really should be sleeping like everyone else.
The bug looks trivial.
“PHP Notice: unserialize(): Error at offset…”
You sigh. You dump the string.
And suddenly you’re looking at that strange, familiar, slightly cursed format:
O:8:"stdClass":1:{s:3:"foo";s:3:"bar";}
Somewhere between the database, Redis, and your own past decisions, you realize:
“Right. We did serialize this. Past me, what were you thinking?”
If this sounds familiar, friends, you and I are in the same tribe.
This is a story about PHP serialization.
Not the superficial “here’s what serialize() does” explanation you’ve seen a hundred times. I want to walk through how it works, where it quietly shapes our architectures, and how it can either make our systems elegant… or give us 3 AM production surprises.
I’ll mix in some practical examples, a bit of code, a few war stories, and some thoughts for people hiring PHP developers or trying to become the kind of PHP dev that doesn’t leave time bombs in the codebase.
Because on Find PHP, that’s what we’re really circling around: reliability, clarity, and craft.
Let’s talk about freezing and thawing state.
What php serialization actually is
If you strip away all the jargon, serialization is just this:
Take a thing in memory → turn it into a string → later, turn it back into the same thing.
In PHP, that usually means:
- You have variables: arrays, objects, scalars.
- You want to store them somewhere external:
- database,
- cache (Redis, Memcached),
- queue,
- session,
- job payloads.
- Or you want to send them to:
- another process on another machine,
- a background worker,
- another microservice (though JSON usually wins here).
So you call:
$serialized = serialize($data);
$original = unserialize($serialized);
Simple, right?
Underneath that simplicity, PHP is doing something very specific:
- It records:
- type (
afor array,sfor string,Ofor object), - lengths,
- class names,
- property names (including visibility),
- nested structures and references.
- type (
That strange little language — with its a:2:{…}, s:5:"hello";, and O:8:"stdClass":… — is PHP’s way of freezing a piece of runtime state and writing it down.
The anatomy of a serialized value
Let’s start with something small.
$value = [
'user_id' => 42,
'name' => 'Ada',
'skills' => ['php', 'symfony'],
];
echo serialize($value);
You’ll see something like:
a:3:{
s:7:"user_id";i:42;
s:4:"name";s:3:"Ada";
s:6:"skills";a:2:{
i:0;s:3:"php";
i:1;s:7:"symfony";
}
}
Behind the noise:
a:3→ array with 3 elementss:7:"user_id";→ string key, length 7, valueuser_idi:42;→ integer 42a:2→ nested array of size 2
It reads noisy, but it’s consistent.
Where it gets really interesting — and sometimes dangerous — is objects.
class User {
public string $name;
private int $id;
public function __construct(int $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
}
$user = new User(1, 'Linus');
echo serialize($user);
You might see something like:
O:4:"User":2:{
s:4:"name";s:5:"Linus";
s:8:"Userid";i:1;
}
Private and protected properties are encoded with special mangled names that include the class and visibility. That matters when you change class names or move properties around — and suddenly your unserialize() doesn’t see what it expects.
Why php serialization refuses to die
If you work in modern PHP — Laravel, Symfony, PSR-everything — you might think serialization is a legacy feature.
It’s not.
Even if you never explicitly call serialize(), chances are:
- Your sessions are serialized (file, Redis, database).
- Your framework’s cache layer serializes complex values.
- Your queues serialize jobs and closures.
- Your ORM or event system might serialize entities, events, or messages.
Many of the tools we love — Laravel’s queue system, Symfony’s cache, various job runners — rely on serialization under the hood.
Serialization is the quiet plumbing.
You don't see it in your feature tickets. You don’t mention it in standups. But it’s one of the invisible contracts of your system: we can freeze objects and thaw them later.
Serialization vs json vs igbinary vs others
If you’re making technical decisions, you eventually face this: what format do we use to store data?
The usual suspects:
serialize()/unserialize()json_encode()/json_decode()- custom encoders (e.g.
igbinary, MessagePack) - framework-specific normalizers/encoders
Here’s how I personally think about it in day-to-day PHP work:
-
JSON:
- Great for public APIs.
- Human-readable.
- Supported by everything.
- But:
- No distinction between arrays/objects the way PHP sees them.
- No automatic preservation of PHP-specific details (class, visibility, references).
- Floats, large integers, and dates need care.
-
PHP serialization:
- Captures exact PHP structure.
- Handles nested arrays, objects, references, even circular ones.
- Perfect for internal caches, sessions, and queues — when you control both ends.
- But:
- Coupled to class definitions.
- Has security concerns if misused.
- Hard to inspect manually.
- Messy when you refactor class names or property visibility.
-
igbinary / others:
- More efficient binary formats.
- Great when you’re optimizing for performance and memory.
- But:
- Less portable.
- Needs extensions installed.
- Makes debugging by hand harder.
In other words:
- For public interfaces and long-lived contracts → JSON or structured formats.
- For internal PHP-to-PHP communication and caches → PHP serialization or binary formats can be fine, if you accept the trade-offs.
If you’re hiring a PHP developer on something like Find PHP, this is the level of thinking you quietly hope they have:
Not just “I can call serialize()”, but “Where does this create coupling or risk five years from now?”
The dark side: php object injection and security
We can’t talk about serialization without talking about the thing that has ruined many a Friday: PHP Object Injection.
Here’s the recipe for disaster:
- You receive some user-controlled string.
- You pass it directly to
unserialize(). - A clever attacker sends a string that instantiates specific classes in your app, with crafted properties.
- Those classes have dangerous
__wakeup()or__destruct()logic:- file deletion,
- network access,
- command execution,
- state changes.
Suddenly, unserialize() becomes remote code execution.
Often the exploited classes aren’t even “vulnerable” in a normal context. They’re just doing something in __wakeup() or __destruct() that makes sense in their intended lifecycle, but is extremely dangerous when triggered with arbitrary data.
So, hard rule:
- Never call
unserialize()on data you do not fully trust. - For data that can be influenced by users, prefer:
- JSON,
- custom safe encoding,
- or at least
unserialize($data, ['allowed_classes' => false])to avoid object creation.
In modern PHP, unserialize() has an options parameter:
$decoded = unserialize($payload, [
'allowed_classes' => false,
]);
This tells PHP: “Don’t resurrect any objects; give me arrays and scalars only.”
Or you can whitelist:
$decoded = unserialize($payload, [
'allowed_classes' => [SomeDto::class, Another::class],
]);
If you’re maintaining older code or interviewing a PHP candidate, a good question is:
“How comfortable are you with
unserialize()and its security implications?”
You’re not trying to trick them. You’re trying to see if they think in terms of attack surfaces, not just features.
And if you’re the developer — it’s a quiet gut check: Do I reach for serialize without thinking what happens if someone tampers with this string?
When serialization starts to hurt: refactors and migrations
Let’s visit that 10:47 PM moment again.
You’re debugging a production issue. The application is trying to unserialize() some user session or cached object. Suddenly:
- Class not found.
- Warnings about property count.
- Logic that assumes a property exists… when it no longer does.
Why?
Because you — or the person before you — did something that felt harmless:
- Renamed a class or moved it to another namespace.
- Changed a property from public to private.
- Deleted an old property and replaced it with a new structure.
- Upgraded a library that changed its internal object layouts.
All those serialized strings still sitting in:
- session stores,
- caches,
- job queues,
- “temporary” tables
… are snapshots of your old reality.
They don’t care that you run PHP 8.3 now and everything is strict and neat. They were frozen when the code looked different.
This is where senior PHP developers quietly earn their paychecks:
Before a big refactor, they ask:
- “Do we have any serialized sessions or cache entries using these classes?”
- “Do we serialize these entities to queues or logs?”
- “Are we storing any serialized objects in long-lived tables?”
And if the answer is “yes”, they plan a migration:
- Periodically warming a new cache and expiring the old one.
- Creating a versioned payload:
type,version,dataso older versions can still be read.
- Providing backward compatibility in constructors or factory methods.
- Implementing custom
__sleep()and__wakeup()to manage how objects are serialized.
Is it tedious? Sometimes.
Is it craftsmanship? Absolutely.
That’s the job: protecting future you from past you.
Sleep, wakeup, and magic around objects
PHP gives you some hooks to control how objects are serialized.
__sleep()
If you define __sleep() on a class, it should return an array of property names to serialize:
class User {
public string $name;
private string $passwordHash;
private $dbConnection;
public function __sleep(): array
{
// We don’t want to serialize connections or sensitive things
return ['name', 'passwordHash'];
}
}
This lets you:
- Exclude non-serializable resources (like database connections, file handles).
- Exclude sensitive data.
- Slim down objects for caching.
__wakeup()
__wakeup() is called on unserialization:
class User {
public function __wakeup()
{
// Reconnect, rehydrate, re-initialize…
}
}
This is powerful.
It’s also why object injection is a problem: if arbitrary serialized data can make arbitrary objects wake up, and those methods do too much, bad things happen.
As a habit:
- Keep
__wakeup()small, predictable, and side-effect-light. - Don’t perform critical actions here that you would not want triggered by malicious data.
In modern architectures, some teams avoid __sleep()/__wakeup() entirely and move towards explicit DTOs and mapping, precisely to control where side effects live.
But if you are working in legacy code — and many of us are — understanding these magic methods is how you keep the system from surprising you.
Php serialization in real projects: where it quietly hides
Let’s ground all of this with something that looks like the systems many of us work on.
Imagine a typical PHP setup:
- Laravel or Symfony app.
- MySQL/PostgreSQL database.
- Redis for:
- cache,
- sessions,
- rate limiting.
- Queue workers (Laravel Horizon, Symfony Messenger, or custom workers).
Where does serialization show up?
-
Sessions
By default, PHP serializes the$_SESSIONarray. Frameworks may wrap it, but the idea is the same: a big associative structure frozen between requests. -
Cache
When you call$cache->set('key', $value), the underlying store often serializes$valueif it isn't a scalar. -
Queues / jobs
Jobs get serialized, then pushed to Redis, database, SQS, etc. The worker pulls, unserializes, and runs them. -
Events / messages
Some event buses or message dispatchers serialize payloads for async handling or logging. -
Custom columns
In older codebases, it’s not uncommon to see aTEXTcolumn in MySQL that contains a serialized array of “options” or “metadata”.
Over time, these patterns create a serialization footprint in your system — a map of where your runtime objects are frozen and stored outside PHP’s memory.
If you’re inheriting a legacy project — or evaluating a candidate for a senior PHP role — one very wholesome exercise is:
“Let’s search the repo for
serialize(andunserialize(and talk through each use.”
You’ll see the history of the system in those lines.
Sometimes you’ll find necessary integration glue. Sometimes you’ll find shortcuts that made sense when the company had three clients and now hurt with three million.
Stable boundaries: from raw serialization to explicit contracts
In more mature projects you start to see a shift:
Instead of doing this:
$payload = serialize($order);
// store $payload
You see something like:
$payload = [
'type' => 'order',
'version' => 2,
'data' => [
'id' => $order->id,
'total' => $order->total,
'currency' => $order->currency,
'items' => $order->items->map(fn($i) => [
'sku' => $i->sku,
'qty' => $i->qty,
'price' => $i->price,
]),
],
];
// Then maybe serialize or json_encode this
$serialized = serialize($payload);
On the other side:
$payload = unserialize($serialized, ['allowed_classes' => false]);
switch ($payload['version']) {
case 1:
// Handle older structure
break;
case 2:
// Handle new structure
break;
}
This looks like more work. It is.
But you gain:
- Versioning: You can read old payloads and handle them differently.
- Decoupling from classes: You’re not storing actual object layouts, just data.
- Safer unserialize:
allowed_classes => falsebecomes natural. - Easier migrations: You can write explicit upgrade logic from v1 to v2.
This is the kind of design that separates “I know PHP” from “I can shape long-lived PHP systems”.
For platforms like Find PHP, which connect people and companies, this difference is very real: it’s the gap between code that merely passes tests and code that survives years of change.
When to reach for php serialization today
So after all this nuance, when is it perfectly fine — even good — to use PHP’s built-in serialization?
Here’s when I use it without guilt:
-
Short-lived caches:
- Data that can be recomputed any time.
- Lifetime in minutes/hours.
- Not an external contract.
-
Internal queues:
- PHP worker → PHP worker, same codebase.
- Jobs that are processed relatively quickly.
- No expectation that payloads survive across major version jumps.
-
CLI-to-CLI communication in PHP tools:
- Temporary orchestration / scripting.
- Developer tooling where you control both ends.
-
Prototypes and experiments:
- Internal tools, short projects, spike solutions.
- Where speed of delivery matters more than long-term evolution.
Where I avoid it or wrap it:
-
Public APIs:
- Always something like JSON, XML, Protocol Buffers — not raw PHP serialization.
-
Long-lived storage contracts:
- Anything that must be readable five years from now with different code.
-
User-controllable fields where data might be edited, imported, or tampered with.
So the mental model becomes:
“Use PHP serialization as an internal optimization detail, not as a public promise.”
Hiring and being hired: what serialization reveals about a developer
Serialization is like an X-ray for how a developer thinks.
If you’re hiring PHP developers using Find PHP (or you’re polishing your own profile there), ask yourself:
- Do they know that
unserialize()can be dangerous? - Have they ever planned a migration that involved old serialized data?
- Do they understand the trade-offs between
serialize()andjson_encode()? - Have they worked with sessions, queues, or caches at scale?
These are practical, grounded questions. The answers rarely come from tutorials. They come from those late-evening sessions in front of the logs.
From the “why did this job fail only after we deployed the refactor?” kind of bugs.
If you’re on the developer side:
- Mention that you’ve:
- migrated legacy serialized payloads,
- dealt with unserialize errors after refactors,
- designed more stable payload formats for queues or APIs.
That tells a hiring manager that you’ve carried real systems, not just toy projects.
A few practical patterns you can use tomorrow
For the sake of leaving you with tools, not just thoughts, here are some quick patterns.
1. Wrap your serialization
Instead of sprinkling serialize() across the codebase:
class Serializer
{
public static function encode(mixed $value): string
{
return serialize($value);
}
public static function decode(string $payload, array $allowedClasses = []): mixed
{
$options = [
'allowed_classes' => empty($allowedClasses) ? false : $allowedClasses,
];
return unserialize($payload, $options);
}
}
Now you have a single place to harden, log, and eventually replace.
2. Use DTOs for cross-boundary messages
final class OrderPlacedMessage
{
public function __construct(
public readonly int $orderId,
public readonly float $total,
public readonly string $currency,
) {}
}
For queues:
- Map your rich domain objects to simple DTOs.
- Serialize the DTOs, not the entire aggregate with all its lazy-loaded relations and odd properties.
Later, if you change the domain model, your DTOs can evolve separately.
3. Handle missing classes gracefully
If you must unserialize() legacy payloads that might contain unknown classes:
-
Consider using
unserialize()withallowed_classes => falseand then:- Inspect the array structure.
- Map manually to new models.
-
Or implement fallback autoloaders or migration scripts that:
- Detect old class names.
- Map them to new ones.
- Transform data formats on-the-fly.
It’s not glamorous work.
But it’s the sort of quiet refactoring that keeps old systems alive without dramatic rewrites.
The quiet art of freezing time
In the end, PHP serialization is not magic.
It’s just us, trying to freeze a moment in time — an object, a request, a job — so that another piece of the system can pick it up later and understand what we meant.
Sometimes we do it carelessly, and those frozen moments come back years later as broken sessions, failing jobs, or security vulnerabilities.
Sometimes we do it with care, and those same moments become stable, reliable contracts between different parts of our code and even different generations of our own thinking.
PHP gives us power here:
- We can freeze anything.
- We can thaw it whenever we want.
- We can design our own rules for what gets remembered and what is discarded.
The question is never just “Should I use serialize() or json_encode()?”
The deeper question is:
“How long do I expect this data to live, and who will have to understand it after me?”
If we keep that in mind — whether we’re building tools, hiring someone through Find PHP, or polishing our own craft as PHP developers — serialization stops being a mysterious bug source.
It becomes what it really is:
A quiet, powerful way to respect time, change, and the people who will read our code when the monitors are glowing and the city outside is already asleep.