This generator can have collisions, although it is extremey unlikely.
If we reduce the byte length and range of random numbers, we can get collisions consistently after a fairly low number of iterations. The values you're using make collisions exponentially less likely, but this shows that it's technically possible. Since you're using time in seconds multiplied between 100 and 999, there are 899 seconds (basically 15 minutes) during which collisions are possible (although highly unlikely) for a given token.
<?php
//Test settings
$byteLength = 3;
$randFloor = 1;
$randCeiling = 5;
$cache = [];
$i = 0;
while (true)
{
$bytes = random_bytes($byteLength);
$int = time() * random_int($randFloor, $randCeiling);
$val = substr('OPT_' . bin2hex($bytes) . $int , 0, 50);
$i++;
if(in_array($val, $cache))
{
echo 'Found collision '.$val.' after '.$i.' iterations'.PHP_EOL;
break;
}
if($i % 100000 == 0)
{
echo $i.' with no collisions...'.PHP_EOL;
}
$cache[] = $val;
}
A few runs:
Found collision OPT_e266e53253149272 after 7251 iterations
Found collision OPT_c6f67e8132873195 after 3572 iterations
Found collision OPT_8156061626574644 after 14993 iterations
Found collision OPT_ccddd76506298584 after 1606 iterations
Found collision OPT_cb06cc1626574650 after 16274 iterations
PHP's built-in uniqid function with "more entropy" does not have this problem, beacuse its result is unique (per thread anyway) for each microsecond. You can see how it works here. Note that this can still be (and may be more of) a problem if you have multiple servers generating IDs, or are generating a LOT of ids at the same time on a system with a bunch of threads/cores. (Time and clock changes can induce more risk of collisions, as can solar flares or stray neutrinos, no purchase necessary, void where prohibited, call your doctor if the condition lasts longer than four hours...)
uniqid('OPT_', true);
Output:
OPT_60f3965b050f39.25070751
You could get the benefits of uniqid
and increase the randomness by using random_bytes to create the prefix. Reduce your number of bytes to set your max length, don't trim off the end.
$bytes = random_bytes(11);
$prefix = 'OPT_' . bin2hex($bytes);
$val = uniqid($prefix, true);
Output:
OPT_5f889773483a0610de61c560f3a57c05dfa5.41225914
OPT_657940118a0e9663c0f86060f3a57c05e825.40200037
OPT_67df31a0252325324e311860f3a57c05f071.69465782
OPT_009f72e62d70b083360e0e60f3a57c05f8e0.85617746
OPT_4d1ca0d26a24bceb4c740460f3a57c0601c4.95161219
What I usually do for things like this is keep track of generated tokens, and check against the list when generating a new one. This reduces the risk of collisions to pretty much zero as long as your storage has strong consistency. If you can do this on your side, you can be confident that you won't send collisions to the API you're calling.
$validToken = false;
while (!$validToken)
{
$token = self::generateToken();
$count = $db->query('SELECT COUNT(*) FROM mytable WHERE token=?', $token);
$validToken = ($count == 0);
}
// Do something with token