drupal_static() is currently as follows (excluding some of the header comments). The code hasn't changed in a decade.
/**
* @param $name
* Globally unique name for the variable. For a function with only one static,
* variable, the function name (e.g. via the PHP magic __FUNCTION__ constant)
* is recommended. For a function with multiple static variables add a
* distinguishing suffix to the function name for each one.
* @param $default_value
* Optional default value.
* @param $reset
* TRUE to reset one or all variables(s). This parameter is only used
* internally and should not be passed in; use drupal_static_reset() instead.
* (This function's return value should not be used when TRUE is passed in.)
*
* @return
* Returns a variable by reference.
*
* @see drupal_static_reset()
*/
function &drupal_static($name, $default_value = NULL, $reset = FALSE) {
static $data = array(), $default = array();
// First check if dealing with a previously defined static variable.
if (isset($data[$name]) || array_key_exists($name, $data)) {
// Non-NULL $name and both $data[$name] and $default[$name] statics exist.
if ($reset) {
// Reset pre-existing static variable to its default value.
$data[$name] = $default[$name];
}
return $data[$name];
}
// Neither $data[$name] nor $default[$name] static variables exist.
if (isset($name)) {
if ($reset) {
// Reset was called before a default is set and yet a variable must be
// returned.
return $data;
}
// First call with new non-NULL $name. Initialize a new static variable.
$default[$name] = $data[$name] = $default_value;
return $data[$name];
}
// Reset all: ($name == NULL). This needs to be done one at a time so that
// references returned by earlier invocations of drupal_static() also get
// reset.
foreach ($default as $name => $value) {
$data[$name] = $value;
}
// As the function returns a reference, the return should always be a
// variable.
return $data;
}
Right at the start, this immediately looks wrong to me:
if (isset($data[$name]) || array_key_exists($name, $data)) {
// Non-NULL $name and both $data[$name] and $default[$name] statics exist.
...
}
(a) That logic is buggy, and certainly does not align with what the comments claim is happening.
(b) If we're going to fall back to testing array_key_exists($name, $data), then testing isset($data[$name]) beforehand is redundant.
(c) isset($data[$name]) can be TRUE when $name = NULL
(d) $default[$name] is not even looked at.
I can't actually tell for certain what the intended logic was, as there are multiple problems. That isset($data[$name]) is definitely wrong, though. We can get away without testing array_key_exists($name, $default) if we have established that array_key_exists($name, $data) (examining the rest of the code confirms that the one follows from the other), but we definitely need to check isset($name) as otherwise we will get this:
>>> drupal_static('', 'foo')
=> "foo"
>>> drupal_static(NULL)
=> "foo"
Which means Drupal can no longer perform a full reset of the static variables, because the interface for doing that is passing $name = NULL.
This happens because $data[NULL] = 'foo' actually gives us ['' => 'foo'] and also array_key_exists(NULL, $data) => TRUE.
I think this code should actually be:
if (isset($name) && array_key_exists($name, $data)) {
// Non-NULL $name, and both $data[$name] and (implicitly)
// $default[$name] statics exist.
...
}
However the following block is also conditional upon isset($name), so we should refactor it all to:
if (isset($name)) {
if (array_key_exists($name, $data)) {
// Non-NULL $name, and both $data[$name] and (implicitly)
// $default[$name] statics exist.
...
}
else {
// Neither $data[$name] nor $default[$name] static variables exist.
...
}
}
and personally I am in favour of also putting the remainder of the function -- the "Reset all: ($name == NULL)" code -- into an else {...} clause for that initial if (isset($name)) statement.
I also think $default should be $defaults (plural) to match with $data (plural).
In all, I propose that the function be refactored like so:
function &drupal_static($name, $default_value = NULL, $reset = FALSE) {
static $data = array(), $defaults = array();
if (isset($name)) {
// Check if we're dealing with a previously defined static variable.
if (array_key_exists($name, $data)) {
// Both $data[$name] and (implicitly) $defaults[$name] statics exist.
if ($reset) {
// Reset pre-existing static variable to its default value.
$data[$name] = $defaults[$name];
}
return $data[$name];
}
else {
// Neither $data[$name] nor $defaults[$name] static variables exist.
if ($reset) {
// Reset was called before any value for $name was set, so we should
// not set anything ($default_value is not reliable in this case). As
// the function returns a reference, we must still return a variable.
// (Code using $reset does not use the return value).
return $data;
}
// First call with new non-NULL $name. Initialize a new static variable.
$defaults[$name] = $data[$name] = $default_value;
return $data[$name];
}
}
else {
// Reset all: ($name == NULL). This needs to be done one at a time so that
// references returned by earlier invocations of drupal_static() also get
// reset.
foreach ($defaults as $name => $value) {
$data[$name] = $value;
}
// As the function returns a reference, we must still return a variable.
return $data;
}
}
| Comment | File | Size | Author |
|---|---|---|---|
| #34 | interdiff-15-34.txt | 545 bytes | jweowu |
| #34 | drupal-10.1.x-core-drupal_static-3136041-34.patch | 4.43 KB | jweowu |
| #12 | drupal-10.1.x-core-drupal_static-3136041-12-TEST-ONLY-FAIL.patch | 1.6 KB | jweowu |
| #4 | drupal_9.1.x-drupal_static-3136041-4.patch | 2.83 KB | jweowu |
| #2 | drupal-drupal_static-3136041-2.patch | 2.85 KB | jweowu |
Comments
Comment #2
jweowu commentedComment #3
jweowu commentedComment #4
jweowu commentedThis is virtually identical in 9.1.x.
Comment #5
jweowu commentedComment #11
smustgrave commentedWill this need a test case for D10?
Comment #12
jweowu commentedSure, here's a 10.1.x re-roll with tests.
Comment #14
smustgrave commentedWow thanks for the quick turnaround!
Only nit picky thing I see (and found thanks to phpstorm)
return $data[$name];Can we move this out of the if/else and just have it outside both. Since both cases return the same thing, would be one less line of code.
Other then that everything looks good!
Great job with the test!
Comment #15
jweowu commentedTrue. I suspect I did that on purpose so that the code for each of those scenarios was very self-contained, but I really can't decide now which way around I prefer it, so here's a revised patch with that change. I think both ways are fine.
Comment #16
smustgrave commentedIt was nit picky thing fully admit. But figured I'd ask now so no one else did down the line. The rest looks good to me.
Comment #18
smustgrave commentedRandom ckeditor5
Comment #19
jweowu commentedThank you for reviewing.
Comment #21
jweowu commentedJust the same irrelevant ckeditor test failure.
Comment #23
jweowu commentedComment #25
jweowu commentedRaised #3346122: ckeditor test failures causing pain for unrelated patches.
Comment #26
jweowu commentedComment #27
catchThe isset() is dropped, but having isset() here was intentional to avoid calling array_key_exists() when the cache is warm, which should be the majority of the times that it's called given it's for static caching. It's not redundant.
Comment #28
jweowu commentedYou're referring to the old code:
if (isset($data[$name]) || array_key_exists($name, $data)) { ... }Functionally that
isset()is redundant; but are you saying that PHP's implementation ofisset($data[$name])is faster than its implementation ofarray_key_exists($name, $data)? If so, that's fine (albeit unexpected) -- but it's visually very weird, so it will need a comment.Moreover, can you confirm whether this performance problem with
array_key_exists()is still true of PHP 8.1+? This code was written a long time ago, after all, so if they've fixed that in the meantime in PHP, then we definitely don't want to add theisset()back again.Comment #29
jweowu commentedhttps://bugs.php.net/bug.php?id=71239 suggests that this performance discrepancy is no longer a going concern:
Comment #30
jweowu commentedComment #31
jweowu commentedComment #32
jweowu commentedNeeds re-roll per https://www.drupal.org/project/drupal/issues/3346645#comment-14959118
Comment #33
jweowu commentedWell... in fact bootstrap.inc isn't a namespaced file, so that was actually still fine -- but perhaps it's sensible to use
\array_key_exists()as standard regardless, just as best-practice.Comment #34
jweowu commentedComment #35
smustgrave commentedSince only change was \array_key_exists() and no failures think this is good.
Comment #37
smustgrave commentedAnother random failure.
Comment #39
smustgrave commentedRandom failure
Comment #40
avpadernoShould not one at a time be once at a time?
Comment #41
jweowu commentedI wouldn't think so. "One at a time" seems appropriate here, as we're doing something to each array item individually.
Comment #42
larowlanComment #44
catcharray_key_exists() isn't the only change here, it's moving the bulk of the logic of the function after an isset($name). The array_key_exists() change is unnecessary, but it's consistent with #3346645: Eliminate anti-pattern isset($arr[$key]) || array_key_exists($key, $arr) now that PHP has optimized array_key_exists(), and hopefully we won't touch this logic again until deprecating per #1577902: [META] Remove all usages of drupal_static() & drupal_static_reset() (and hopefully sooner than another ten years).
Made this change on commit since we usually rely on git blame for links back to issues. Committed/pushed to 10.1.x, thanks!
Comment #45
jweowu commentedThanks all. (And thanks smustgrave for keeping on top of the false-positive status updates.)