SplStack, SplQueue, SplPriorityQueue: Using SPL Data Structures in Practice
AI generated
8.4
PHP · SPL · Data Structures
SplStack, SplQueue, and SplPriorityQueue in Practice
When built-in data structures beat the plain array

A PHP array can serve as a stack, a queue, and almost anything else, that flexibility is its greatest strength and, at the same time, its biggest weakness. The SPL ships SplStack, SplQueue, and SplPriorityQueue, specialized structures that not only express intent more clearly in code but also outperform array_shift and array_push under the right conditions. We show when the switch actually pays off.

12 min read SplStack SplQueue SplPriorityQueue Doubly Linked List

1. Why an array isn't always the right choice

PHP arrays are ordered hashmaps with built-in support for array_push, array_pop, array_shift, and array_unshift. That means you can technically rebuild any stack or queue semantics with them, and that is exactly what many developers do without thinking through the consequences. The problem isn't functionality, it comes down to two things: performance at scale and the lack of semantic clarity in the code itself.

Anyone reading array_push($stack, $item) has to infer from the variable name and surrounding context that stack semantics are intended here. Nothing stops another developer from accidentally calling array_shift on that same variable and silently changing the order. A SplStack instance, by contrast, only exposes methods that fit stack semantics, making that class of mistake impossible through the type signature alone.

2. The shared foundation: SplDoublyLinkedList

SplStack and SplQueue are both specializations of SplDoublyLinkedList, a doubly linked list. Every node knows its predecessor and successor, which makes inserting and removing at either end possible in constant time, regardless of the list's length. Contrast that with a PHP array, which internally also relies on a hashtable with linked buckets, but which still has to reindex all numeric keys on array_shift.

That reindexing is the actual cost driver: array_shift doesn't just remove the first element, it implicitly shifts every subsequent numeric index down by one. On an array with a hundred thousand elements, a single array_shift call therefore means walking almost the entire structure. SplQueue sidesteps this problem entirely because no element needs to know or maintain its own index.


$list = new SplDoublyLinkedList();
$list->push('a');
$list->push('b');
$list->unshift('z'); // O(1), no reindexing required

foreach ($list as $item) {
    echo $item . PHP_EOL; // z, a, b
}

3. SplStack: implementing undo functionality cleanly

A classic use case for a stack is an undo feature, for example in an editor for content blocks or a CLI tool with several reversible steps. SplStack extends the doubly linked list with LIFO iteration order and the methods push, pop, and top, all of which run in constant time. The code ends up reading almost like a specification, without anyone having to guess which end of an array counts as the current end of the history.

It matters that SplStack references objects rather than copying them. If you push mutable objects onto the stack and keep mutating them afterward, you're also mutating the stored state. For a genuine undo history you should either use immutable state objects or clone explicitly before pushing, otherwise a later pop() returns an already mutated state instead of the original one.


final class UndoHistory
{
    private SplStack $stack;

    public function __construct()
    {
        $this->stack = new SplStack();
    }

    public function record(EditorState $state): void
    {
        // Cloning is mandatory, otherwise pop() later returns the mutated state
        $this->stack->push(clone $state);
    }

    public function undo(): ?EditorState
    {
        return $this->stack->isEmpty() ? null : $this->stack->pop();
    }
}

4. SplQueue: FIFO processing without reindexing cost

For a simple task queue without prioritization, for example sequentially processing incoming webhook events within a request, SplQueue is a good fit. The class extends the same doubly linked list but exposes enqueue and dequeue as semantically clear FIFO methods. Internally, enqueue simply calls push and dequeue simply calls shift on the base class, both in constant time, without any of an array's reindexing overhead.

A practical example is processing several mutually dependent import batches where the order absolutely must be preserved. Instead of an array with a manual array_shift inside a loop, SplQueue makes the code's intent explicit while staying performant even with thousands of batches, because each dequeue operation is independent of the queue's remaining size.


$queue = new SplQueue();
foreach ($importBatches as $batch) {
    $queue->enqueue($batch);
}

while (!$queue->isEmpty()) {
    $batch = $queue->dequeue(); // O(1), FIFO order preserved
    $importer->process($batch);
}

5. SplPriorityQueue: processing tasks by urgency

Once the processing order no longer depends purely on time but on a priority, SplPriorityQueue comes into play. Internally the class uses a max heap, so inserting an element runs in logarithmic time and extracting the highest-priority element also runs in logarithmic time. An array would need either a full sort on every insert, which scales quadratically overall, or a linear scan for the maximum on every extract to achieve the same behavior.

This matters in practice for something like a job queue where critical maintenance tasks must run before regular report generation, regardless of the order in which they were enqueued. By default, extract() only returns the value. Via setExtractFlags() you can change that so both value and priority, or an array containing both, are returned, which is almost always more useful in practice.


$queue = new SplPriorityQueue();
$queue->setExtractFlags(SplPriorityQueue::EXTR_BOTH);

$queue->insert('report:monthly', 1);
$queue->insert('maintenance:disk-cleanup', 10);
$queue->insert('report:daily', 3);

while (!$queue->isEmpty()) {
    $item = $queue->extract();
    echo "{$item['data']} (priority {$item['priority']})" . PHP_EOL;
}
// Output: maintenance:disk-cleanup (10), report:daily (3), report:monthly (1)

6. Stability with equal priority and custom comparison logic

One detail that's easy to overlook in practice: SplPriorityQueue does not guarantee a stable order by insertion time for elements with equal priority. Two elements with identical priority can be extracted in either order, because the underlying heap only knows priority as its sort criterion. If you need deterministic ordering on ties, extend the priority with a second, finer component, for example a descending timestamp.

For more complex comparison logic, you can also extend SplPriorityQueue with a subclass that overrides compare(). That's useful when priority isn't a simple integer but is composed of several fields on an object. Alternatively, for pure min-heap use cases the related class SplMinHeap is a good fit, while SplMaxHeap matches the default behavior of SplPriorityQueue minus the extra priority dimension.


final class DeterministicPriorityQueue extends SplPriorityQueue
{
    public function compare(mixed $priority1, mixed $priority2): int
    {
        // On ties, the secondary component (timestamp) decides
        return $priority1 <=> $priority2;
    }
}

7. Performance head to head against array functions

The difference between array_shift and SplQueue::dequeue() only becomes visible at realistic data volumes. With small lists of a few hundred elements the difference is barely measurable, because both operations sit in the microsecond range. Past tens of thousands of elements, a clear trend emerges: an array that repeatedly has elements removed from the front degrades linearly per operation as it grows, while SplQueue stays constant.

The important caveat is that this advantage applies exclusively to operations at the ends. For random access via a numeric index, an array is still clearly ahead, because accessing a hashtable element runs in constant time, while a linked list may in the worst case have to walk the entire list for the same access. So the choice of data structure should always be driven by the actual access pattern, not by a blanket preference.

8. Understanding iteration mode and memory behavior

A detail that's often overlooked with SplDoublyLinkedList and its descendants is the configurable iteration mode. Via setIteratorMode() you can control whether iteration consumes elements, emptying the stack or queue in the process, or leaves the contents unchanged. The default mode, IT_MODE_LIFO on SplStack and IT_MODE_FIFO on SplQueue, doesn't remove elements by default, but combining it with IT_MODE_DELETE is a deliberate option for destructive processing.

Regarding memory footprint: a doubly linked list needs extra memory per element for the predecessor and successor references, while a PHP array can be organized more compactly internally. For very large datasets that are rarely manipulated at the ends, a plain array can therefore even be more memory-efficient. The SPL structures earn their advantage specifically with frequent insert and remove operations at the ends, not as a general memory optimization.

9. Decision guide: which structure fits which use case

In practice, SplStack pays off whenever you need clear LIFO semantics with frequent push and pop operations on a growing structure, for example undo histories, bracket validation in parsers, or recursive traversals reworked into an iterative form. SplQueue fits FIFO scenarios instead, such as sequential batch processing or simple queues without any prioritization requirement.

SplPriorityQueue is the right choice as soon as a ranking exists that goes beyond plain insertion order, for example job schedulers, event loops with varying urgency, or Dijkstra-like graph algorithms. For small, manageable lists with mixed access patterns, a plain array often remains the most pragmatic choice, because the extra object overhead of the SPL structures only pays off at meaningful size or with frequent manipulation at the ends.

Structure Operation Complexity Typical use case
Array array_shift O(n), reindexing required Small lists, rare removal at the front
SplStack push / pop O(1) Undo history, bracket validation
SplQueue enqueue / dequeue O(1) Sequential batch processing
SplPriorityQueue insert / extract O(log n) Job scheduler, prioritized task queue
SplMinHeap / SplMaxHeap insert / extract O(log n) Pure min or max heap applications

Mironsoft

PHP modernization, code quality, and legacy refactoring

Grown PHP code nobody wants to touch anymore?

We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.

Legacy Refactoring

Modernize grown PHP code in a structured, low-risk way.

Establishing Code Quality

Anchor PHPStan, coding standards, and CI checks sustainably in the team.

Version Upgrades

Plan and execute PHP major version upgrades safely, without downtime.

10. Summary

SPL data structures: the essentials at a glance

Foundation

SplStack and SplQueue inherit from SplDoublyLinkedList with O(1) operations at both ends.

Semantics

Push, pop, enqueue, and dequeue express intent more clearly than generic array functions.

Priority

SplPriorityQueue uses a max heap for O(log n) insert and extract by urgency.

Limits

For random index access, a plain array still outperforms a linked list.

11. FAQ: SPL data structures: the essentials at a glance

1When is SplStack worth it over a plain array?
Whenever push and pop operations are frequent and the code should clearly express stack semantics. For small, rarely manipulated lists the difference is negligible.
2Why is array_shift slow on large arrays?
After removing the first element, array_shift has to reindex all subsequent numeric keys, which on large arrays means walking nearly the entire structure.
3What's the difference between SplQueue and SplStack?
Both are based on SplDoublyLinkedList but differ in iteration direction and exposed methods: SplStack is LIFO with push and pop, SplQueue is FIFO with enqueue and dequeue.
4Does SplPriorityQueue guarantee stable order on equal priority?
No. With identical priority, extraction order is not guaranteed. For determinism, extend the priority with a second component such as a timestamp.
5How do I configure SplPriorityQueue to return value and priority?
Via setExtractFlags with the value SplPriorityQueue::EXTR_BOTH, extract() returns an array with the keys data and priority instead of just the raw value.
6Can I customize the comparison logic of SplPriorityQueue?
Yes, by subclassing and overriding the compare method. That's useful when priority is composed of several fields on an object instead of a simple integer.
7Is SplStack more memory-efficient than an array?
Not inherently. A doubly linked list needs extra memory per element for predecessor and successor references, its advantage lies in time complexity at the ends, not in memory footprint.
8What happens during iteration with IT_MODE_DELETE?
Elements are removed from the structure as iteration proceeds. This is deliberately destructive and fits processing where the structure should end up empty anyway.
9Should I clone mutable objects before pushing them onto a SplStack?
Mutable objects should be cloned before pushing, otherwise a later pop() returns the already mutated state instead of the originally stored one.
10When does a plain array remain the better choice?
For random access via numeric indices and for small, manageable lists with mixed access patterns, a plain array is usually more pragmatic than an SPL structure.