Data Types And Standard Library

Randomness

Random values appear in password reset links, email verification tokens, API keys, CSRF tokens, invitation codes, temporary filenames, test data, lotteries, games, shuffles, sampling, and simulations. PHP gives you several tools for these jobs, but they are not interchangeable.

The most important distinction is security. If a value protects an account, session, payment, private file, or permission, use PHP's cryptographically secure functions: random_bytes() and random_int(). If you need repeatable numbers for a test, game replay, simulation, or tutorial, use a pseudo-random number generator with an explicit seed and understand that predictability is the point.

Randomness is not one idea. It has at least three common meanings in PHP work:

Security randomness: unpredictable values for secrets and tokens.
Pseudo-randomness: deterministic sequences that look mixed enough for non-secret uses.
Repeatable randomness: seeded pseudo-randomness for tests and simulations.

Confusing those meanings can create serious bugs. The most dangerous mistake is using a predictable pseudo-random generator for secrets.

True Randomness And Practical Randomness

A computer program is deterministic by default. Given the same code and the same state, it usually produces the same result. That is useful for debugging, but it means a program cannot simply invent perfect randomness from nothing.

When developers say a PHP value is random, they usually mean one of two things. A cryptographically secure random value comes from an operating-system source designed to be unpredictable. A pseudo-random value comes from an algorithm that expands a seed into a sequence that appears mixed but is still deterministic.

Neither phrase should be used casually. A pseudo-random number generator can be excellent for games, examples, randomized UI tests, load-test data, and simulations. It can still be completely wrong for reset tokens. A cryptographically secure generator is appropriate for secrets, but it is not normally seeded by your application because manual seeding would weaken the point.

Use the generator that matches the risk. If guessing the value gives an attacker access, money, private data, or permission, treat the value as a secret and use secure randomness.

PRNG Means Pseudo-Random Number Generator

A PRNG is an algorithm that keeps internal state and produces a sequence of values from that state. The sequence can look random, but it is generated by rules. If an attacker knows the algorithm and the state, or can infer enough of the state, future values may be predictable.

A seed initializes the generator's state. The same seed with the same generator can produce the same sequence. That is useful when you want repeatability. It is dangerous when you need unpredictability.

This example uses the older Mersenne Twister functions to show the idea of deterministic seeding:

PHP example
<?php

declare(strict_types=1);

mt_srand(1234);
$firstRun = [mt_rand(1, 100), mt_rand(1, 100), mt_rand(1, 100)];

mt_srand(1234);
$secondRun = [mt_rand(1, 100), mt_rand(1, 100), mt_rand(1, 100)];

echo $firstRun === $secondRun ? 'same sequence' : 'different sequence';
echo PHP_EOL;

// Prints:
// same sequence

The exact numbers are less important than the property: resetting the generator to the same seed reproduces the sequence. That is not a security feature. It is a reproducibility feature.

Seeding For Repeatable Tests

Seeded pseudo-randomness is useful when a test needs varied-looking data but must produce the same result on every run. For example, a tutorial might generate fake product scores, a simulation might need replayable events, or a test might need a deterministic shuffle.

In modern PHP, the Random\Randomizer API lets you choose an engine explicitly. This makes the dependency clearer than calling global functions throughout the application.

PHP example
<?php

declare(strict_types=1);

use Random\Engine\Mt19937;
use Random\Randomizer;

function sampleScores(int $seed): array
{
    $randomizer = new Randomizer(new Mt19937($seed));

    return [
        $randomizer->getInt(1, 10),
        $randomizer->getInt(1, 10),
        $randomizer->getInt(1, 10),
    ];
}

echo sampleScores(42) === sampleScores(42) ? 'repeatable' : 'not repeatable';
echo PHP_EOL;
echo sampleScores(42) === sampleScores(99) ? 'same' : 'different';
echo PHP_EOL;

// Prints:
// repeatable
// different

This is a good pattern for non-secret repeatability because the seed is visible and intentional. A reviewer can see that the code is designed to reproduce a sequence.

Do not reuse this pattern for account security. A seeded PRNG is attractive to attackers because repeatability means predictability when the seed or state can be guessed.

Do Not Seed Secure Randomness Yourself

random_bytes() and random_int() are designed for cryptographic use. They use secure system randomness through PHP's random extension and operating-system facilities. You do not pass a seed to them, and you should not try to make them deterministic for production.

If a secure token is hard to test because it changes every run, test its properties instead: length, format, range, uniqueness constraints, storage behavior, and rejection behavior. Do not replace secure randomness with mt_rand() just so a test can assert one exact string.

This is the correct shape for a password reset token:

PHP example
<?php

declare(strict_types=1);

function passwordResetToken(): string
{
    return bin2hex(random_bytes(32));
}

$token = passwordResetToken();

echo strlen($token) . PHP_EOL;
echo ctype_xdigit($token) ? 'hex' : 'not hex';
echo PHP_EOL;

// Prints:
// 64
// hex

The token changes every run. That is the point. A test should not expect the same token twice.

Generate Secure Tokens

Use random_bytes() when you need unpredictable bytes, then encode them into a string that can travel through URLs, emails, or databases.

PHP example
<?php

declare(strict_types=1);

$token = bin2hex(random_bytes(32));

echo 'Token length: ' . strlen($token) . PHP_EOL;
echo 'Looks like hex: ' . (ctype_xdigit($token) ? 'yes' : 'no') . PHP_EOL;

// Prints:
// Token length: 64
// Looks like hex: yes

The actual token changes every run. The reliable checks are its length and format. For high-value tokens, store only a hash of the token when possible, just as you would avoid storing a password in plain text.

Generate Secure Integers

Use random_int() when you need an integer inside a range.

PHP example
<?php

declare(strict_types=1);

$roll = random_int(1, 6);

echo 'Roll is valid: ' . ($roll >= 1 && $roll <= 6 ? 'yes' : 'no') . PHP_EOL;

// Prints:
// Roll is valid: yes

This is suitable for security-sensitive numeric choices too, such as backup code digits or short-lived verification codes. random_int() returns a uniformly selected integer in the requested range, which avoids common modulo-bias mistakes.

Format Numeric Codes Carefully

Verification codes often need leading zeroes. Generate a number, then format it as a fixed-width string.

PHP example
<?php

declare(strict_types=1);

function verificationCode(): string
{
    return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
}

$code = verificationCode();

echo 'Code length: ' . strlen($code) . PHP_EOL;
echo 'Only digits: ' . (ctype_digit($code) ? 'yes' : 'no') . PHP_EOL;

// Prints:
// Code length: 6
// Only digits: yes

Do not cast the final code back to an integer, because that would remove leading zeroes. Also remember that short numeric codes need rate limits, expiry, and attempt limits. Six digits are easy for a user to type, but they are not enough protection if an attacker can try unlimited guesses.

Avoid Predictable Functions For Secrets

Older functions such as rand() and mt_rand() are not the right choice for tokens, passwords, sessions, invite links, password reset URLs, CSRF tokens, or anything an attacker could benefit from guessing.

This is unsafe for a reset token:

PHP example
<?php

declare(strict_types=1);

mt_srand(2026);

$badToken = (string) mt_rand(100000, 999999);

echo strlen($badToken) . PHP_EOL;

// Prints:
// 6

The problem is not only that the token is short. It is also generated by a PRNG whose sequence is reproducible when seeded. If a developer seeds with the current day, user id, process id, timestamp, or another guessable value, an attacker may be able to reduce the search space dramatically.

Use a secure token instead:

PHP example
<?php

declare(strict_types=1);

function secureInviteToken(): string
{
    return bin2hex(random_bytes(16));
}

$token = secureInviteToken();

echo strlen($token) . PHP_EOL;

// Prints:
// 32

The function name says what the random value is for. That helps reviewers spot when security-sensitive code is using the wrong tool.

How Predictable Randomness Gets Hacked

Predictable randomness creates attacks because the attacker does not need to break encryption or guess every possible value. They only need to learn enough about how the value was generated.

Common mistakes include using time() as a seed, using a user id as a seed, generating short codes with no rate limit, using mt_rand() for password reset tokens, logging tokens, returning tokens in error responses, or using the same deterministic sequence in every process.

Imagine a reset token generated like this:

PHP example
<?php

declare(strict_types=1);

mt_srand(time());

echo mt_rand(100000, 999999) . PHP_EOL;

If an attacker knows roughly when the token was created, they can try seeds from that small time window and reproduce likely codes. This is why seeded PRNGs are useful for simulations but dangerous for secrets.

The repair is not to hide the seed better. The repair is to use secure randomness, add expiry, limit attempts, monitor abuse, and store sensitive tokens carefully.

Random Filenames Should Not Expose Original Names

Temporary and stored filenames often need randomness to avoid collisions and hide user-supplied names.

PHP example
<?php

declare(strict_types=1);

function randomStorageName(string $extension): string
{
    $extension = strtolower($extension);

    if (!preg_match('/^[a-z0-9]+$/', $extension)) {
        throw new InvalidArgumentException('Invalid extension.');
    }

    return bin2hex(random_bytes(16)) . '.' . $extension;
}

$filename = randomStorageName('pdf');

echo str_ends_with($filename, '.pdf') ? 'pdf name' : 'wrong extension';
echo PHP_EOL;

// Prints:
// pdf name

The original filename can be stored as metadata for display, but it should not control the storage path. Do not rely on randomness alone for authorization. A private file still needs an access check.

Test Random Code By Properties

Random output changes, so tests should check promises that are always true.

PHP example
<?php

declare(strict_types=1);

function inviteCode(): string
{
    return strtoupper(bin2hex(random_bytes(4)));
}

$code = inviteCode();

$isValid = strlen($code) === 8 && ctype_xdigit($code) && strtoupper($code) === $code;

echo $isValid ? 'valid invite code' : 'invalid invite code';
echo PHP_EOL;

// Prints:
// valid invite code

For code that needs deterministic randomness in tests, inject a generator or use a Randomizer with a known PRNG engine and seed. Do not weaken production randomness just to make tests easier.

A simple application boundary can accept a Randomizer for non-secret sampling:

PHP example
<?php

declare(strict_types=1);

use Random\Engine\Mt19937;
use Random\Randomizer;

function pickGreeting(Randomizer $randomizer): string
{
    $greetings = ['Hi', 'Hello', 'Welcome'];

    return $greetings[$randomizer->getInt(0, count($greetings) - 1)];
}

$testRandomizer = new Randomizer(new Mt19937(7));

echo in_array(pickGreeting($testRandomizer), ['Hi', 'Hello', 'Welcome'], true) ? 'valid' : 'invalid';
echo PHP_EOL;

// Prints:
// valid

This makes the randomness dependency visible without touching secure token generation.

Bias And Guessing Space

Randomness also has a shape. A generator may produce values across a large internal range, but your program usually maps those values into a smaller application range: a six-digit code, an array index, a dice roll, or a filename suffix. If that mapping is careless, some outputs can become more likely than others. This is called bias.

For security-sensitive integers, prefer the secure integer API because it handles uniform range selection for you. Avoid shortcuts such as taking a random number and using the remainder operator to force it into a range unless you understand the bias that can create. Bias may not matter for a toy animation, but it matters when an attacker can make many guesses.

Guessing space is the other side of the same problem. A six-digit code has only one million possible values before expiry, and fewer if leading zeroes are accidentally removed. That can be acceptable only when the surrounding system adds expiry, attempt limits, monitoring, and account-specific checks. The randomness function is not the whole security control.

For tokens sent in URLs, use enough random bytes that online guessing is unrealistic, then encode them safely. For short human-entered codes, accept that usability requires a smaller space and compensate with operational controls. Do not confuse a convenient code with a strong secret.

Choose The Right Tool

Use this decision rule:

Security token, password reset, CSRF token, API key, session secret: random_bytes().
Security-sensitive integer or verification code: random_int(), plus expiry and rate limits.
Game, shuffle, fake data, simulation, repeatable test: Randomizer with an explicit PRNG engine and seed.
Legacy non-secret code: rand() or mt_rand() may appear, but do not use them for new secrets.

The dangerous part of randomness is that wrong code can appear to work. A predictable token still looks random to a casual observer. A short code still works for a real user. A seeded PRNG still produces different-looking values. Security depends on what an attacker can predict, not on whether the output looks mixed to you.

What To Remember

Use random_bytes() for secure tokens and random_int() for secure numbers. A PRNG is deterministic; seeding it can deliberately reproduce the same sequence. That is useful for tests, simulations, games, and examples, but dangerous for secrets. Do not seed secure randomness yourself. Check random output by length, format, and range. Keep generated secrets out of logs, store only hashes when appropriate, add expiry and attempt limits for short codes, and avoid predictable generators for anything security-sensitive.

Official References

Practice

Task: Generate account recovery values

Write a small example that generates values for an account recovery flow.

Requirements

  • Use declare(strict_types=1);.
  • Generate a password reset token using random_bytes().
  • Encode the token as hexadecimal.
  • Generate a six-digit verification code using random_int().
  • Preserve leading zeroes in the verification code.
  • Print checks for token length, token format, code length, and code format.
  • Include the expected output as comments in the same PHP code block.

Do not print the real token as the main proof. Randomness should be checked by properties that stay true every run.

Show solution
PHP example
<?php

declare(strict_types=1);

function passwordResetToken(): string
{
    return bin2hex(random_bytes(32));
}

function verificationCode(): string
{
    return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
}

$token = passwordResetToken();
$code = verificationCode();

echo 'Token length: ' . strlen($token) . PHP_EOL;
echo 'Token is hex: ' . (ctype_xdigit($token) ? 'yes' : 'no') . PHP_EOL;
echo 'Code length: ' . strlen($code) . PHP_EOL;
echo 'Code is digits: ' . (ctype_digit($code) ? 'yes' : 'no') . PHP_EOL;

// Prints:
// Token length: 64
// Token is hex: yes
// Code length: 6
// Code is digits: yes

The real values change every run, so the example verifies stable properties instead of comparing exact strings. In a production recovery flow, the raw reset token should be shown to the user once and a hash should be stored for later comparison.