David Wyly David Wyly - 7 months ago 46
PHP Question

multiple uniqid() calls not being unique

I have an interesting example where multiple, repeated calls to

uniqid()
are not generating unique numbers when hosted locally on XAMPP. The unique id is repeated anywhere between 5-20 times and, then, mysteriously changes.

However, as an interesting twist, the code works perfectly on our production server.

So here's what I'm doing: I'm creating a wrapper that, when clicked, hides/unhides the child content in a div via a simple javascript function. Since the hideable div is being generated dynamically, it is referenced by a unique id that is generated by PHP.

An example of the issue is as follows:

// Replace something like '[element] => <newline> (' with <a href="javascript:toggleDisplay('[unique id]');">...</a><div id="[unique id]" style="display: none;">
$out = preg_replace_callback(
$regex,
function ($matches) {
$id = uniqid();
return $matches[1] . "<a class='debug' href='javascript:toggleDisplay(\"" . $id . "\");'>" . $matches[2] . "</a>" . "<div id='" . $id . "' style='display: none'>";
}, $out
);


The javascript function is as follows (just so you can see what I'm doing; it works perfectly):

<script language="Javascript">
function toggleDisplay(id) {
document.getElementById(id).style.display = (document.getElementById(id).style.display == "block") ? "none" : "block";
}
</script>'


The problem is that the output divs all have the same unique id (!!), in clusters anywhere between 5-15, so the javascript doesn't know what div to reference.

So some things I have found: If I do something like
$id = uniqid() . rand(10000,99999)
instead of just
$id = uniqid()
then the code works again as intended. So I'm pretty sure that the problem is that
uniqid()
is not actually generating a unique id, considering that I'm not overwriting or reusing the
$id
variable.

Another interesting thing I have found: if I echo the
microtime()
along with the
uniqid()
, the
uniqid()
changes only when the
microtime()
changes. To me this feels like a clue.

So my question is: Why would
uniqid()
only sometimes generate a
uniqid()
? Isn't
uniqid()
supposed to generate a unique number, even if the
microtime()
is the same? Is this behavior documented or well known? Or is there something else that I'm missing?

I ask because I'm feeling uncomfortable using
uniqid()
because I'm not understanding the core behavior.

Any insight would be appreciated. Thank you.

Answer

The result of uniqid() is not guaranteed to be unique, and your investigation with microtime() is indeed a clue as to why.

According to the manual page for uniqid(), it:

Gets a prefixed unique identifier based on the current time in microseconds.

So the main input is indeed the current "microtime". However, it also takes an extra parameter:

more_entropy If set to TRUE, uniqid() will add additional entropy (using the combined linear congruential generator) at the end of the return value, which increases the likelihood that the result will be unique.

Note that even with this argument, the manual is careful not to guarantee uniqueness, but as with your manual use of rand(), it is adding an extra source of randomness which makes collisions vastly more unlikely.

To confirm, we can look at the source code for the function, where we can see that the output without more_entropy set is indeed just a hex representation of the current microsecond timestamp. An interesting piece to notice is this:

#if HAVE_USLEEP && !defined(PHP_WIN32)
    if (!more_entropy) {
#if defined(__CYGWIN__)
        php_error_docref(NULL, E_WARNING, "You must use 'more entropy' under CYGWIN");
        RETURN_FALSE;
#else
        usleep(1);
#endif
    }
#endif

So, if you're not under Windows, the function will actually try to sleep for a microsecond in order to force subsequent values to be different.

This makes it a bad idea to run uniqid() lots of times in succession, because if it does succeed, it will do so slowly. (Requiring either a microsecond of sleep, or a call to the random-number generator.)

A better idea is to use it once to generate an arbitrary prefix, and then simply increment a counter for each item, which could look something like this:

$id_prefix = uniqid();
$id_suffix = 0;
$out = preg_replace_callback(
        $regex,
        function ($matches) use ($id_prefix, &$id_suffix) {
            $id = $id_prefix . $id_suffix;
            $id_suffix ++;
            return $matches[1] . '... some html ...' . $id . ' ... ';
        },
        $out
);
Comments