A couple years ago I watched a perfectly reasonable feature request turn into a subtle data bug: “Randomize the recommended articles.” Someone reached for shuffle(), shipped it, and a day later analytics looked haunted. The bug wasn’t in the randomness at all—it was in the array keys.
That’s the real story of shuffle() in PHP: it’s simple, fast, and great at one job, but it has a sharp edge that will cut you if you treat it like “randomize this list without changing anything else.” The function shuffles values and then reindexes the array, which is exactly what you want for lists and exactly what you don’t want for many associative arrays.
I’ll walk you through what shuffle() actually guarantees, how it behaves with keys, how I handle security-sensitive shuffling (spoiler: not with shuffle()), and how I make randomness reproducible in tests and experiments. By the end you’ll know when shuffle() is the right tool, when to reach for a different pattern, and how to avoid the classic mistakes that still show up in production PHP codebases.
What shuffle() really does (and what it returns)
The PHP signature is deceptively small:
<?php
/
* shuffle(array &$array): bool
*/
A few details matter in real code:
- It shuffles the array in place. The array is passed by reference, so the original variable is modified.
- It returns a boolean, not the shuffled array.
truemeans the operation succeeded,falsemeans it failed. - It reindexes keys starting from 0. This is not a side effect; it’s part of the contract.
- On modern PHP (8.x), invalid input becomes a
TypeError. Historically you’d see warnings/falsey behavior in older versions. In strict codebases, assume type errors for non-arrays.
Here’s the “I’ve seen this in reviews” mistake:
<?php
$names = ["Ram", "Shita", "Geeta"];
$shuffled = shuffle($names); // WRONG: $shuffled is bool
var_dump($shuffled);
print_r($names);
The correct approach is:
<?php
$names = ["Ram", "Shita", "Geeta"];
if (!shuffle($names)) {
throw new RuntimeException(‘shuffle() failed‘);
}
print_r($names);
In practice, shuffle() almost always succeeds for a normal array, but I still like the explicit check in code that sits in a critical path or has to be very defensive (batch jobs, import pipelines, or anything that handles unexpected input).
A quick mental model that helps: shuffle() is like taking index cards with values written on them, tossing them in the air, and then stacking them back into a new pile labeled 0..n-1.
What does “in place” really mean in PHP?
When I say “in place,” I mean “the variable you pass is modified.” That sounds obvious until you’re dealing with arrays passed through multiple layers.
- If you pass
$itemsinto a function that callsshuffle($items), the caller’s array is modified (because the parameter is passed by reference insideshuffle(), but you still pass the variable in as normal). - If you want to avoid surprises, copy intentionally and shuffle the copy.
<?php
function getRecommendations(array $articles): array
{
$shuffled = $articles; // explicit copy
shuffle($shuffled);
return array_slice($shuffled, 0, 5);
}
I’ve found that this “copy then shuffle” pattern pays for itself in reduced debugging time, especially when multiple callers share the same array.
What does shuffle() guarantee?
This is a subtle but important point for how you reason about correctness.
- It produces a permutation of the input values.
- It reindexes numeric keys starting at 0.
- It does not promise reproducibility across runs.
- It does not promise cryptographic unpredictability.
That last pair is where people overreach: they use shuffle() for experiments they want to replay, or for attacker-facing randomness. That’s when you should switch tools.
The key behavior that surprises people (associative arrays lose keys)
If you take only one thing from this post, take this:
shuffle() discards existing keys and replaces them with numeric keys starting at 0.
That means the function is perfect for “lists” and dangerous for “maps.”
Demonstration: associative array turns into a list
<?php
$teamByRole = [
‘backend‘ => ‘Asha‘,
‘frontend‘ => ‘Mateo‘,
‘qa‘ => ‘Noor‘,
‘devops‘ => ‘Inez‘,
];
shuffle($teamByRole);
print_r($teamByRole);
Output will resemble:
Array
(
[0] => Noor [1] => Mateo [2] => Inez [3] => Asha)
Notice what’s missing: the roles. If your code later does $teamByRole[‘qa‘], it’s now broken.
Pattern: shuffle while preserving keys
When I actually want “random order, same key/value pairs,” I shuffle the keys, then rebuild:
<?php
$teamByRole = [
‘backend‘ => ‘Asha‘,
‘frontend‘ => ‘Mateo‘,
‘qa‘ => ‘Noor‘,
‘devops‘ => ‘Inez‘,
];
$keys = array_keys($teamByRole);
shuffle($keys);
$shuffled = [];
foreach ($keys as $k) {
$shuffled[$k] = $teamByRole[$k];
}
print_r($shuffled);
This keeps ‘backend‘, ‘frontend‘, etc. intact.
Pattern: shuffle objects without losing identity
If you store objects in a list, shuffle() is usually fine because identity is in the object, not the key:
<?php
final class Article {
public function construct(
public readonly string $id,
public readonly string $title,
) {}
}
$articles = [
new Article(‘art_1001‘, ‘Scaling background jobs‘),
new Article(‘art_1002‘, ‘PHP arrays: the good parts‘),
new Article(‘art_1003‘, ‘Incident response checklist‘),
];
shuffle($articles);
foreach ($articles as $a) {
echo $a->id . " – " . $a->title . "\n";
}
That’s the safe “list semantics” sweet spot.
Make the “list vs map” distinction explicit (my favorite defensive trick)
A lot of production bugs happen because a variable is “usually a list” until it isn’t. I like to enforce list semantics before shuffling.
If you’re on PHP versions that support it, arrayislist($array) is a clean check. If you’re not, you can approximate: a list has integer keys 0..n-1 in order.
<?php
function assertlistarray(array $items, string $label = ‘items‘): void
{
if (functionexists(‘arrayis_list‘)) {
if (!arrayislist($items)) {
throw new InvalidArgumentException($label . ‘ must be a list (0..n-1 keys).‘);
}
return;
}
$i = 0;
foreach ($items as $k => $_) {
if ($k !== $i) {
throw new InvalidArgumentException($label . ‘ must be a list (0..n-1 keys).‘);
}
$i++;
}
}
$items = [‘a‘ => 1, ‘b‘ => 2];
assertlistarray($items, ‘items‘); // throws
I don’t use this everywhere, but I absolutely use it in library-like code where you don’t control callers.
When shuffle() is the wrong tool (security and fairness)
There are two categories where I avoid shuffle():
- Security-sensitive randomness (anything attacker-facing)
- Fairness-sensitive selection (anything you must be able to justify and reproduce)
Why I don’t use shuffle() for security
shuffle() uses PHP’s general-purpose pseudo-randomness. That’s fine for UI variety—rotating testimonials, randomizing a quiz, shuffling a playlist. It’s not what I want for:
- password reset flows
- invitation codes
- shuffling that influences money (promo eligibility, payouts)
- anything where an attacker could benefit from predicting outcomes
If you’re in those situations, I recommend implementing a Fisher-Yates shuffle using random_int().
A cryptographically safer shuffle (Fisher-Yates with random_int())
This keeps list semantics (numeric keys) and uses a stronger source of randomness:
<?php
/
* @template T
* @param array $items A list with 0..n-1 keys
* @return array
*/
function cryptoshufflelist(array $items): array
{
// Fisher-Yates shuffle
for ($i = count($items) – 1; $i > 0; $i–) {
$j = random_int(0, $i);
if ($i !== $j) {
$tmp = $items[$i];
$items[$i] = $items[$j];
$items[$j] = $tmp;
}
}
return $items;
}
$prizes = [
‘Prize A: Keyboard‘,
‘Prize B: Headphones‘,
‘Prize C: Gift card‘,
‘Prize D: Sticker pack‘,
];
$shuffled = cryptoshufflelist($prizes);
print_r($shuffled);
A couple important constraints:
- I require
array(a list). If you have an associative array, decide whether you want to preserve keys or not and convert deliberately. - This returns a new array; it doesn’t mutate the caller’s variable. That’s a design choice I prefer in security-sensitive code because it reduces “who changed my data?” debugging.
“Fairness” means you can explain and replay
If you’re running experiments (rotations, assignments, content ordering) where stakeholders will ask “Why did this user see that?”, you need reproducibility. That leads directly into seeded randomness.
I also like to log what matters:
- the seed
- the population size
- (optionally) the resulting first N IDs
That way, if something looks off later, you can reproduce exactly what happened instead of guessing.
Reproducible shuffles for tests and experiments (seeded randomness)
Non-deterministic tests are a slow-moving disaster. I’ve seen teams lose hours every week to flaky suites caused by randomness that wasn’t pinned down.
In 2026-era PHP, you usually have two clean options:
- Use newer randomness APIs in your runtime for deterministic shuffling
- Implement a seeded shuffle yourself
I’ll show the second option because it works everywhere and makes the behavior obvious.
A deterministic shuffle you can seed
This example uses hash() to generate a repeatable stream of integers from a seed string. It’s not cryptographic security; it’s about reproducibility.
<?php
/
* Deterministically shuffle a list based on a seed.
*
* @template T
* @param array $items
* @param string $seed Any stable identifier (experiment id, date bucket, user id)
* @return array
*/
function seededshufflelist(array $items, string $seed): array
{
// Fisher-Yates, but our "random" choice comes from a seed-derived digest.
$n = count($items);
for ($i = $n – 1; $i > 0; $i–) {
// Create a deterministic digest for each step.
$digest = hash(‘sha256‘, $seed . ‘|‘ . $i, true);
// Convert first 4 bytes to an unsigned 32-bit integer.
$num = unpack(‘N‘, substr($digest, 0, 4))[1];
$j = $num % ($i + 1);
if ($i !== $j) {
$tmp = $items[$i];
$items[$i] = $items[$j];
$items[$j] = $tmp;
}
}
return $items;
}
$cards = [
‘On-call handbook‘,
‘Deploy checklist‘,
‘Code review rubric‘,
‘Service ownership notes‘,
‘Incident timeline template‘,
];
$seed = ‘exprecommendationsv3
2026-02-09‘;
$shuffled = seededshufflelist($cards, $seed);
print_r($shuffled);
Now you can:
- reproduce a user’s ordering later
- run stable snapshot tests
- compare two implementations without noise
Traditional vs modern approach (what I recommend)
Traditional approach
—
shuffle($items) in the controller
Rely on runtime randomness
(experimentid, userid, time_bucket) and use deterministic shuffling shuffle() and hope
random_int() or platform RNG; log inputs; make it auditable If you only need “variety,” keep it simple. If you need “explainable,” seed it.
Stable “random” ordering across pages (a practical UX problem)
One common product requirement is:
- Page 1 shows items in a random order.
- Page 2 shows the next items in that same random order.
- Refreshing doesn’t reshuffle (at least not for some time window).
If you call shuffle() per request, you break pagination. The fix is to make the ordering a deterministic function of a seed.
Here’s the simplest version:
<?php
/
* @param list $itemIds
* @return list
*/
function stablerandomorderforuser(array $itemIds, string $userId, string $bucket): array
{
$seed = ‘stable_order
‘ . $bucket;
return seededshufflelist($itemIds, $seed);
}
$ids = [‘a1‘,‘a2‘,‘a3‘,‘a4‘,‘a5‘,‘a6‘,‘a7‘,‘a8‘];
$ordered = stablerandomorderforuser($ids, ‘user_42‘, ‘2026-W06‘);
$page1 = array_slice($ordered, 0, 3);
$page2 = array_slice($ordered, 3, 3);
That $bucket is the knob that controls how often the order changes. I’ve used:
- a daily bucket (
2026-02-09) - a weekly bucket (
2026-W06) - a “session” bucket (a session identifier)
The important bit is that you can control UX consistency and still have variety.
Real-world patterns that work well with shuffle()
This section is where I see shuffle() shine: short-lived shuffling of list-like data.
Pattern: randomize a playlist, then take the first N items
<?php
$playlist = [
‘Night Drive (Remaster)‘,
‘Ocean Signal‘,
‘Paper Satellites‘,
‘Late Coffee‘,
‘City Rain‘,
‘Static Bloom‘,
‘Midnight Build‘,
];
shuffle($playlist);
$nextUp = array_slice($playlist, 0, 3);
print_r($nextUp);
I like this for “show me 3 random things” when the list is not huge.
Pattern: rotate featured items but keep a “pinned first” rule
Sometimes you want controlled randomness: one item must remain first, the rest can vary.
<?php
$featured = [
‘Company status page‘,
‘Incident guide‘,
‘Support portal‘,
‘Release notes‘,
‘Security contact‘,
];
$pinned = array_shift($featured); // remove first item
shuffle($featured);
array_unshift($featured, $pinned);
print_r($featured);
Pattern: shuffle questions, then score
<?php
$questions = [
[‘prompt‘ => ‘What does shuffle() return?‘,
‘answer‘ => ‘bool‘,
],
[‘prompt‘ => ‘Does shuffle() preserve associative keys?‘,
‘answer‘ => ‘no‘,
],
[‘prompt‘ => ‘Does shuffle() modify the array?‘,
‘answer‘ => ‘yes‘,
],
];
shuffle($questions);
foreach ($questions as $i => $q) {
echo ($i + 1) . ‘. ‘ . $q[‘prompt‘] . "\n";
}
This is a perfect use case: you want variety, you don’t care about keys, and the data is already list-shaped.
Pattern: pick N random items without shuffling everything
If you only need a small sample, shuffle() + array_slice() can be wasteful on large arrays.
Two alternatives I use a lot:
array_rand()to pick random keys- A partial Fisher-Yates shuffle to randomize only the first N positions
Here’s array_rand() for a sample without replacement:
<?php
/
* @template T
* @param list $items
* @return list
*/
function pickrandomitems(array $items, int $n): array
{
if ($n <= 0) {
return [];
}
$count = count($items);
if ($count === 0) {
return [];
}
if ($n >= $count) {
$copy = $items;
shuffle($copy);
return $copy;
}
// array_rand returns int|array; normalize to array
$keys = array_rand($items, $n);
$keys = is_array($keys) ? $keys : [$keys];
$out = [];
foreach ($keys as $k) {
$out[] = $items[$k];
}
return $out;
}
This keeps the original array intact, and it avoids shuffling everything. If your list is huge, it can be a meaningful win.
If you want the chosen items in random order too, you can shuffle the output list (small) after picking.
Performance notes I actually care about
Most shuffle() calls I review are not the bottleneck. Still, there are performance and memory realities worth knowing.
Time complexity: linear in the number of elements
Shuffling is typically O(n). If you have 10,000 items, the shuffle itself is usually quick. If you have 5,000,000 items, the shuffle is not the only problem—you also have memory pressure from storing that array in the first place.
Memory behavior: shuffle() works in place
Because shuffle() mutates the existing array, it tends to be memory-friendlier than approaches that copy and rebuild large arrays. The tradeoff is that in-place mutation can surprise callers.
If I’m working in a codebase that prefers immutability at boundaries, I’ll often copy intentionally:
<?php
$items = getLargeList();
$copy = $items; // explicit copy so callers don‘t get surprises
shuffle($copy);
That makes the cost visible.
Avoid shuffling huge lists just to pick a few items
If you need 10 random items out of 10 million, shuffling the entire array is the wrong move.
When I can’t push this down into the database (for example, selecting random rows with constraints), I’ll do one of these:
- Pick random indices with
random_int()until I have enough unique picks (works when you need a small sample) - Use reservoir sampling if the data is streamed
Here’s a reservoir sampling example that takes a random sample of size $k from a generator without loading everything into memory:
<?php
/
* @template T
* @param iterable $stream
* @param int $k
* @return list
*/
function reservoir_sample(iterable $stream, int $k): array
{
if ($k <= 0) {
return [];
}
$sample = [];
$i = 0;
foreach ($stream as $item) {
$i++;
if (count($sample) < $k) {
$sample[] = $item;
continue;
}
$j = random_int(1, $i);
if ($j <= $k) {
$sample[$j – 1] = $item;
}
}
return $sample;
}
function streamOrders(): Generator {
// Imagine reading orders from a cursor or API
for ($n = 1; $n <= 100000; $n++) {
yield ‘order_‘ . $n;
}
}
$tenRandomOrders = reservoir_sample(streamOrders(), 10);
print_r($tenRandomOrders);
That’s not shuffle(), but it solves the “random subset” problem in a way that stays fast and memory-stable.
Database note (because people always ask)
I’ll keep this pragmatic: “random rows” in a database is a separate topic, but here’s the rule of thumb I use.
- If the dataset is small and it’s not a hot path, pulling IDs and shuffling in PHP is often fine.
- If the dataset is large or it’s a hot path, naive “random order” queries tend to get expensive.
In those cases, I prefer patterns that avoid full random sorts:
- precompute a random-ish ordering key per row (or per day/week)
- select via random offsets inside an indexed range
- select candidates by hash bucketing (e.g.,
hash(id + seed)idea) and then shuffle in PHP
The key is: avoid forcing the database to fully sort a huge candidate set purely for randomness.
Common mistakes and gotchas (the stuff I warn teams about)
Here are the issues I see repeatedly in real code reviews.
Mistake 1: expecting keys to survive
If you start with:
<?php
$pricesBySku = [‘SKU123‘ => 19.99, ‘SKU124‘ => 24.50];
shuffle($pricesBySku);
you no longer have SKUs. If you need key/value pairs, shuffle keys and rebuild.
Mistake 2: using shuffle() inside a sort comparator
Sometimes people try:
<?php
usort($items, fn() => random_int(-1, 1));
That’s a bug magnet. Sort algorithms expect the comparator to be consistent; randomness violates that assumption and can produce biased or unstable results. If you want a random permutation of a list, use shuffle() or Fisher-Yates.
If you want “random but stable,” assign each item a deterministic score (like a hash-derived number) and sort by that score.
Mistake 3: shuffling while iterating
Shuffling an array you’re currently iterating over leads to confusion fast. If you need to iterate randomly, shuffle first, then iterate.
Mistake 4: assuming “random” means “evenly distributed” for every run
A good shuffle produces a uniform distribution across many runs, not a guarantee that “this run looks evenly mixed.” Humans are bad at eyeballing randomness. If you need statistical guarantees, test them with a quick script and a sanity check, not by staring at sample output.
Mistake 5: forgetting that shuffle() is global-state randomness
If your app seeds the global PRNG for any reason, it can influence shuffles elsewhere. In modern PHP codebases, I prefer containing randomness behind a service so tests can control it.
A simple pattern I use is: all application-level shuffling goes through a Shuffler that I can swap in tests.
A production-friendly Shuffler abstraction (contain randomness, stop surprises)
This is the section I wish I’d written earlier in my career, because it’s the difference between “randomness sprinkled everywhere” and “randomness that behaves.”
I like three properties:
- A small API: “shuffle a list.”
- A default implementation for normal UI variety.
- Alternative implementations for security (
random_int()) and determinism (seeded).
Here’s a clean baseline.
Step 1: define a tiny interface
<?php
interface Shuffler
{
/
* @template T
* @param list $items
* @return list
*/
public function shuffle(array $items): array;
}
Notice that I accept and return a list, not “any array.” That’s intentional: it forces callers to decide whether keys matter.
Step 2: the default shuffle() implementation
<?php
final class PhpShuffleShuffler implements Shuffler
{
public function shuffle(array $items): array
{
// Defensive: if you want, assert list-ness here.
// assertlistarray($items, ‘items‘);
shuffle($items);
return $items;
}
}
This keeps the mutation contained to the local copy and returns the shuffled list.
Step 3: a crypto-oriented shuffler
<?php
final class CryptoShuffler implements Shuffler
{
public function shuffle(array $items): array
{
for ($i = count($items) – 1; $i > 0; $i–) {
$j = random_int(0, $i);
if ($i !== $j) {
$tmp = $items[$i];
$items[$i] = $items[$j];
$items[$j] = $tmp;
}
}
return $items;
}
}
Now I can wire CryptoShuffler only where it matters (money, access, adversarial environments), and leave everything else using the default.
Step 4: a seeded shuffler for tests and experiments
<?php
final class SeededShuffler implements Shuffler
{
public function construct(private string $seed)
{
}
public function shuffle(array $items): array
{
$n = count($items);
for ($i = $n – 1; $i > 0; $i–) {
$digest = hash(‘sha256‘, $this->seed . ‘|‘ . $i, true);
$num = unpack(‘N‘, substr($digest, 0, 4))[1];
$j = $num % ($i + 1);
if ($i !== $j) {
$tmp = $items[$i];
$items[$i] = $items[$j];
$items[$j] = $tmp;
}
}
return $items;
}
}
In tests, I’ll do:
<?php
$shuffler = new SeededShuffler(‘test-seed‘);
$out = $shuffler->shuffle([1,2,3,4,5]);
// assertSame([3,1,5,4,2], $out); // example; pick the real expected output
And now your “random” tests are deterministic.
Step 5: use it in an application service
<?php
final class Recommendations
{
public function construct(private Shuffler $shuffler)
{
}
/
* @param list
* @return list
*/
public function pick(array $articles, int $limit): array
{
$shuffled = $this->shuffler->shuffle($articles);
return array_slice($shuffled, 0, $limit);
}
}
This is the point: the rest of your codebase no longer cares how randomness works.
Edge cases I actually see in the wild
A few scenarios where shuffle() itself is fine, but the surrounding data model causes trouble.
Edge case: arrays that are “mostly list” but have gaps
PHP arrays can have numeric keys that aren’t contiguous.
<?php
$items = [
0 => ‘a‘,
2 => ‘b‘,
3 => ‘c‘,
];
shuffle($items);
After shuffling, you’ll get a clean 0..n-1 list. That might be good (you wanted a list), or it might hide a bug (your data already had a structural issue). If gaps are meaningful to you, don’t shuffle until you understand why you have gaps.
Edge case: associative arrays where you care about both keys and order
Sometimes you genuinely want:
- keys preserved
- values attached to keys
- a randomized iteration order
In that case, shuffle keys and rebuild (as shown earlier). I also like wrapping it to make intent obvious:
<?php
/
* @template K of array-key
* @template V
* @param array $map
* @return array
*/
function shufflepreservekeys(array $map): array
{
$keys = array_keys($map);
shuffle($keys);
$out = [];
foreach ($keys as $k) {
$out[$k] = $map[$k];
}
return $out;
}
The value here isn’t the code; it’s the name. It prevents future readers from assuming keys survive a plain shuffle().
Edge case: “random order” but with weights
Sometimes product wants “random” but not uniform—new items should appear more often, or sponsored items should be more likely.
At that point, shuffle() isn’t the right primitive. You’re not looking for a uniform random permutation; you’re looking for a weighted random ordering.
A practical approach I use:
- assign each item a score based on weight and randomness
- sort by score
One classic technique is to generate a key like -log(U)/w (where U is uniform random in (0,1] and w is the weight). Smaller scores come first. It’s a solid way to get a weighted random ordering without repeating items.
I won’t pretend this belongs in every app, but it’s a useful tool when “shuffle” isn’t enough.
A quick decision checklist (what I do before I type shuffle())
When I’m about to randomize a collection, I ask:
- Is this a list (0..n-1 keys), or a map (meaningful keys)?
- Do I need reproducibility (tests, experiments, supportability)?
- Is this security-sensitive or attacker-facing?
- Do I need a full permutation, or just a small sample?
- Is the dataset large enough that shuffling it is wasteful?
And I choose:
shuffle($items)for simple UI variety on lists- “shuffle keys and rebuild” when keys matter
- Fisher-Yates +
random_int()for security-sensitive list shuffling - a seeded shuffle for deterministic results
- sampling methods (
array_rand(), reservoir sampling) when I only need a few
Final takeaways
I still like shuffle()—a lot. It’s a great default when you’re working with lists and you want cheap variety. The trouble starts when you treat it as “randomize this structure while keeping it otherwise the same.” It won’t: it reindexes keys, it mutates the input variable, and it’s not designed for cryptographic or auditable fairness.
If you make the list-vs-map decision explicit, contain randomness behind a small abstraction, and use seeded shuffles for tests and experiments, you can keep the convenience of shuffle() without inheriting its sharp edges.


