Für einen Buchstabensalat im Deutschunterricht soll aus „Bär“ ein „räB“ werden und kein Wort mit Fragezeichen-Rauten. Arrays mischt shuffle(), einzelne Elemente zieht array_rand(), und wie man Zufall wiederholbar macht, zeigt mt_srand(). PHP str_shuffle() ist für die Zeichen eines Strings zuständig, mit zwei Fallstricken, die man kennen sollte, bevor man es für Umlaute oder Gutscheincodes einsetzt.
Der eine zerlegt Umlaute, der andere macht Passwörter erratbar.
Was PHP str_shuffle() macht
Die Funktion bekommt einen String und gibt einen neuen zurück, in dem dieselben Zeichen in anderer Reihenfolge stehen. Das Original bleibt unverändert:
<?php
$str = 'abcdef';
$shuffled = str_shuffle($str);
echo $shuffled; /* z. B. edcbaf */
echo $str; /* abcdef */
Ein leerer String bleibt leer, ein einzelnes Zeichen bleibt, wie es ist. Anders als shuffle(), das ein Array an Ort und Stelle umbaut und nur true meldet, arbeitet PHP str_shuffle() mit einer Kopie.
Die Signatur im Original
Ein Parameter, ein Rückgabewert:
str_shuffle(string $string): string
/* $string the input string
returns the shuffled string */
Seit PHP 7.1 kommt der Zufall aus dem Mersenne-Twister, vorher aus rand() der C-Bibliothek. Eine Zahl als Argument wird ohne Meldung zum String. Ein null erzeugt seit PHP 8.1 eine Deprecation-Meldung und liefert den leeren String.
Permutation: dieselben Zeichen in neuer Reihenfolge
Mathematisch ist das Ergebnis eine Permutation, also eine von allen möglichen Anordnungen. Es kommt kein Zeichen hinzu, und keines geht verloren. count_chars() bestätigt das:
<?php
$vorher = count_chars($str, 1);
$nachher = count_chars($shuffled, 1);
var_dump($vorher === $nachher); /* bool(true) */
Doppelte Buchstaben bleiben doppelt. Aus 'aab' entstanden in 3000 Läufen nur aab, aba und baa. Und das Ergebnis darf dem Original gleichen: Bei 'abc' passierte das in rund einem Sechstel der Fälle. Jede Anordnung ist dabei gleich wahrscheinlich, wie ein Lauf mit 600.000 Aufrufen zeigt:
| Ergebnis für 'abc' | Windows | Linux |
| abc | 16,77 % | 16,72 % |
| acb | 16,60 % | 16,69 % |
| bac | 16,66 % | 16,59 % |
| bca | 16,68 % | 16,65 % |
| cab | 16,69 % | 16,70 % |
| cba | 16,60 % | 16,65 % |
Erwartet sind 16,67 Prozent je Zeile. Alle Permutationen eines Wortes auflisten kann PHP str_shuffle() dagegen nicht, es liefert bei jedem Aufruf genau eine. Bei sechs verschiedenen Zeichen gibt es 720 davon.
Umlaute und UTF-8: PHP str_shuffle() mischt Bytes
Ein „ü“ belegt in UTF-8 zwei Bytes, ein „ß“ ebenfalls. Die Funktion kennt keine Zeichen, nur Bytes, und reißt solche Paare auseinander. Mit festem Startwert, damit das Ergebnis nachprüfbar bleibt, sieht das so aus:
<?php
mt_srand(7);
$salat = str_shuffle('Grüße');
echo bin2hex($salat); /* 47bc65c372c39f */
var_dump(mb_check_encoding($salat, 'UTF-8'));
/* bool(false) */
var_dump(json_encode($salat)); /* bool(false) */
echo json_last_error_msg();
/* Malformed UTF-8 characters, possibly
incorrectly encoded */
Im Browser erscheinen an diesen Stellen Ersatzzeichen, und eine JSON-API liefert statt einer Antwort false. Bei „Äpfel“ waren 832 von 1000 Ergebnissen kein gültiges UTF-8. Rechnerisch landen die beiden Bytes des Ä nur in einem von sechs Fällen in der richtigen Reihenfolge direkt hintereinander. Die Lösung zerlegt den Text zuerst in Zeichen und mischt dann diese:
<?php
function zeichen_mischen(string $text): string
{
$zeichen = mb_str_split($text);
shuffle($zeichen);
return implode('', $zeichen);
}
echo zeichen_mischen('Grüße'); /* z. B. ßeGrü */
Mehr zu mb_str_split() und seinen Verwandten steht im Tutorial über Multibyte-Strings. Für Emojis reicht auch das nicht immer. Ein Daumen mit Hautfarbe besteht aus zwei Codepunkten, eine Flagge ebenfalls. mb_str_split() zerlegte den Text aus beiden in vier Teile, grapheme_str_split() aus der intl-Erweiterung von PHP 8.4 in die zwei sichtbaren Zeichen. Die Mischfunktion von oben braucht dafür nur eine andere Zerlegung:
<?php
function grapheme_mischen(string $text): string
{
$teile = grapheme_str_split($text);
shuffle($teile);
return implode('', $teile);
}
/* PHP 8.4 mit intl */
Unter Linux mit intl blieben Daumen, Flagge und „Bär“ in 1000 Läufen immer ganz. Dem Windows-PHP des Tests fehlte intl.
Der Zufall dahinter: Mt19937 und mt_srand()
PHP str_shuffle() nimmt seinen Zufall aus dem globalen Mersenne-Twister, denselben, den auch mt_rand(), shuffle() und array_rand() nutzen. Ein Startwert legt deshalb alle fest:
<?php
mt_srand(42);
echo str_shuffle('abcdefghij'); /* bdjhgaiefc */
mt_srand(42);
echo str_shuffle('abcdefghij'); /* bdjhgaiefc */
Das Ergebnis war unter Windows und Linux identisch. Ein einzelnes mt_rand() zwischen Startwert und Mischen verschob es auf dcbiafegjh, ein random_int() dagegen nicht, denn diese Funktion nutzt eine andere Quelle. Ruft also irgendeine Bibliothek mt_srand(42) auf, liefert PHP str_shuffle() danach bei jedem Programmlauf dieselbe Folge von Ergebnissen. Wie man den Startwert gezielt einsetzt, zeigt das Tutorial zu mt_srand().
Wer reproduzierbare Ergebnisse für Tests braucht, ohne den Rest der Anwendung zu beeinflussen, gibt dem Zufall eine eigene Instanz:
<?php
use Random\Randomizer;
use Random\Engine\Mt19937;
$r = new Randomizer(new Mt19937(42));
echo $r->shuffleBytes('abcdefghij'); /* bdjhgaiefc */
Das Verfahren ist dasselbe, das Ergebnis auch, nur der globale Zustand bleibt unberührt. Achtung: Wie der Name sagt, mischt auch shuffleBytes() Bytes und zerlegt Umlaute.
Warum PHP str_shuffle() keine Passwörter erzeugt
In vielen Foren steht substr(str_shuffle($alphabet), 0, 10) als Passwortgenerator. Häufig wird dagegen eingewandt, dass sich darin kein Zeichen wiederholt. Das stimmt, in 20.000 solchen Passwörtern gab es keine einzige Wiederholung. Nachgerechnet kostet das bei 62 Zeichen und zehn Stellen aber nur gut ein Bit, 58,4 statt 59,5. Das eigentliche Problem liegt woanders:
<?php
$alphabet = 'abcdefghijklmnopqrstuvwxyz'
. 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
mt_srand(1);
$a = substr(str_shuffle($alphabet), 0, 12);
mt_srand(1 + 2 ** 32);
$b = substr(str_shuffle($alphabet), 0, 12);
var_dump($a === $b); /* bool(true) */
echo $a; /* 53o29MkLQuva */
Der Mersenne-Twister nimmt nur einen Startwert mit 32 Bit an, höhere Bits fallen weg. Es gibt also höchstens rund 4,3 Milliarden verschiedene Folgen, egal wie lang das Passwort ist. Das Handbuch zu mt_srand() nennt das ausdrücklich und rechnet vor, dass sich nach weniger als 80.000 zufälligen Startwerten mit 50 Prozent Wahrscheinlichkeit einer wiederholt. Setzt ein Programmteil den Startwert aus der Uhrzeit, lässt sich das Passwort sogar nachbauen. Und ist das Alphabet kürzer als gewünscht, kürzt substr() das Ergebnis still: Aus 'abc' werden drei Zeichen statt zehn.
Für Passwörter und Gutscheincodes gibt es sichere Wege. Ab PHP 8.3 wählt getBytesFromString() die Zeichen mit der sicheren Zufallsquelle, davor erledigt random_int() das in einer Schleife:
<?php
$alphabet = 'abcdefghijkmnpqrstuvwxyz'
. 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$r = new Random\Randomizer();
$code = $r->getBytesFromString($alphabet, 12);
/* vor PHP 8.3 */
$code = '';
$max = strlen($alphabet) - 1;
for ($i = 0; $i < 12; $i++) {
$code .= $alphabet[random_int(0, $max)];
}
Im Alphabet fehlen absichtlich leicht verwechselbare Zeichen wie 0, O, 1, l und I. Auch getBytesFromString() wählt Bytes, das Alphabet besteht deshalb nur aus ASCII-Zeichen. Wiederholungen gehören hier dazu: Von 20.000 Codes mit zwölf Stellen aus diesem Alphabet enthielten rund 72 Prozent mindestens eine. Weitere sichere Kennungen mit random_bytes() zeigt das Tutorial zu uniqid() und seinen Alternativen.
str_shuffle(), shuffle() und der Randomizer im Vergleich
Zum Mischen und Ziehen gibt es mehrere Funktionen, die gern verwechselt werden:
| Funktion | Gibt zurück | Original | Zufallsquelle |
str_shuffle() | neuen String | bleibt | globaler Mt19937 |
shuffle() | true | wird umgebaut | globaler Mt19937 |
array_rand() | Schlüssel | bleibt | globaler Mt19937 |
shuffleBytes() | neuen String | bleibt | eigene Engine, Standard sicher |
shuffleArray() | neues Array | bleibt | eigene Engine, Standard sicher |
Ein new Random\Randomizer() ohne Argument nutzt die Engine Secure. Wann shuffle() und wann array_rand() das Richtige ist, vergleicht das Tutorial zu array_rand() ausführlich. Welche Variante zum Mischen von Zeichen passt, hängt an drei Fragen:
flowchart TD
A[Zeichen eines Strings mischen]
B{Ergebnis muss geheim bleiben?}
C[Randomizer mit Engine Secure]
D{Umlaute, Akzente oder Emojis?}
E[erst in Zeichen teilen, dann mischen]
F{reproduzierbar nötig?}
G[Randomizer mit Mt19937 und Startwert]
H[str_shuffle genügt]
A --> B
B -->|ja| C
B -->|nein| D
D -->|ja| E
D -->|nein| F
F -->|ja| G
F -->|nein| H
Praxis: Buchstabensalat und gemischtes Ziffernfeld
Zurück zum Unterricht. Für das Spiel müssen Umlaute heil bleiben, das gemischte Wort soll sich vom Original unterscheiden, und ein Wort wie „aa“ darf keine Endlosschleife auslösen:
<?php
function buchstabensalat(string $wort): ?string
{
$zeichen = mb_str_split($wort);
if (count(array_unique($zeichen)) < 2) {
return null; /* nichts zu mischen */
}
$r = new Random\Randomizer();
for ($i = 0; $i < 100; $i++) {
$neu = implode('', $r->shuffleArray($zeichen));
if ($neu !== $wort) {
return $neu;
}
}
return null;
}
var_dump(buchstabensalat('aa')); /* NULL */
echo buchstabensalat('Bär'); /* z. B. räB */
Manchmal ist gerade die Eigenschaft gewünscht, dass jedes Zeichen genau einmal vorkommt. Ein PIN-Feld, dessen Ziffern bei jedem Aufruf anders angeordnet sind, erschwert das Mitlesen von Fingerbewegungen. Weil es dabei um Sicherheit geht, kommt der Zufall aus der sicheren Engine statt aus PHP str_shuffle():
<?php
$r = new Random\Randomizer();
$tasten = str_split($r->shuffleBytes('0123456789'));
foreach ($tasten as $ziffer) {
echo '<button>' . $ziffer . '</button>';
}
Fazit
Für Spiele, Rätsel und Testdaten mit einfachen Buchstaben ist PHP str_shuffle() ein bequemer Einzeiler. Das Ergebnis ist eine gleichverteilte Permutation, das Original bleibt erhalten, und mit einem festen Startwert lässt es sich wiederholen, am saubersten über eine eigene Mt19937-Instanz.
Die Grenzen liegen bei Multibyte-Text und bei Geheimnissen. Umlaute und Emojis brauchen mb_str_split() oder grapheme_str_split() vor dem Mischen. Für Passwörter, Codes und alles andere, das niemand erraten soll, taugt der 32-Bit-Startwert nicht, dort gehören random_int() oder der Randomizer mit sicherer Engine hin.