Problem/Motivation

In bootstrap.php we define the REQUEST_TIME constant so that PHPUnit test work. This is fine whilst we are not adding integration and functional test suites to PHPUnit - but we are:
#2232861: Create BrowserTestBase for web-testing on top of Mink
#2304461: KernelTestBaseTNG™

Both these issues do something like:

+++ b/core/includes/bootstrap.inc
@@ -86,7 +86,9 @@
-define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
+if (!defined('REQUEST_TIME')) {
+  define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
+}

@@ -114,7 +116,9 @@
-define('DRUPAL_ROOT', dirname(dirname(__DIR__)));
+if (!defined('DRUPAL_ROOT')) {
+  define('DRUPAL_ROOT', dirname(dirname(__DIR__)));
+}

Up to now when we were unit testing functionality that relies on the request time we would define the REQUEST_TIME constant at the start of the test, in core/tests/bootstrap.php. This would ensure that we do not get 'undefined constant' notices, and that the tests have a somewhat realistic value to work with. This works fine for proper unit tests that only load the class under test and do not actually perform a full bootstrap of Drupal.

However now that we are about to introduce running functional tests through PHPUnit this would mean that Drupal would be fully bootstrapped after PHPUnit runs our bootstrap.php. In the 'real' bootstrap the REQUEST_TIME constant would be defined again, throwing a notice that would fail the test, resulting in the workaround above.

Proposed resolution

No longer rely on the REQUEST_TIME constant In FooTest code so it can be removed from bootstrap.php and will never conflict with bootstrap.inc. Instead fallback to $_SERVER['request_time'] directly. In actual code use the information which is stored in the request object. Some code might run really early in the installer, so we have to fallback to the REQUEST_TIME from bootstrap.incthere.

Remaining tasks

User interface changes

API changes

This is about improving testability but there a couple of non changes to remove dependency on REQUEST_TIME where that code is already unit tested.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task
Issue priority Major because it is blocking to major issues.
Unfrozen changes Unfrozen because it doesn't change API's. It changes OO code to be more testable and touches test related code which is not frozen.
Disruption Minimal. It does not affect any API's but could break tests outside of core using REQUEST_TIME

Comments

alexpott’s picture

Status: Active » Needs review
Issue tags: +PHPUnit
Related issues: +#2232861: Create BrowserTestBase for web-testing on top of Mink, +#2304461: KernelTestBaseTNG™
StatusFileSize
new19.13 KB

Status: Needs review » Needs work

The last submitted patch, 1: 2459155.1.patch, failed testing.

pfrenssen’s picture

Title: Remove REQUEST_TIME from boostrap.php » Remove REQUEST_TIME from bootstrap.php
alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new2.11 KB
new21.32 KB

Fixing the patch.

dawehner’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 4: 2459155.4.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new829 bytes
new22.13 KB

Last fix :)

neclimdul’s picture

I'm ok with the change personally but we might want to make sure we bring this to the attention of DX minded people sooner rather then later and make sure everyone is on board.

+++ b/core/tests/bootstrap.php
@@ -82,7 +82,7 @@ function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $l
-define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
+//define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);

I assume for review/testing that its only commented out?

Crell’s picture

If it's in $_SERVER, then it should be in the request object, no? We should get it from there rather than the super-global.

We could arguably stick it into the container as a parameter, too, in which case services that need it can just get that injected. We do that for the root dir now, don't we? Or is that different because it doesn't change per-request?

alexpott’s picture

StatusFileSize
new504 bytes
new22.17 KB

Thanks for the review @neclimdul.

I'm not sure what DX has to do with this. If you have code that you want to be unit testable and you have dependencies on $_SERVER variables use the request object.

neclimdul’s picture

Because I didn't realize bootstrap.php is not bootstrap.inc.

+++ b/core/lib/Drupal/Core/Cache/MemoryBackend.php
@@ -202,6 +202,20 @@ public function removeBin() {
+    if (defined('REQUEST_TIME')) {
+      return REQUEST_TIME;
+    }
+    else {
+      return (int) $_SERVER['REQUEST_TIME'];
+    }

could we just return (int) $_SERVER['REQUEST_TIME'];? I don't know there might be a reason not to but seems like it'd be fine to me.

neclimdul’s picture

Issue summary: View changes

beta eval to clarify

pfrenssen’s picture

StatusFileSize
new22.18 KB
pfrenssen’s picture

Issue summary: View changes

I was also confused about the difference between bootstrap.inc and bootstrap.php. @alexpott explained it to me. Updated issue summary.

pfrenssen’s picture

Status: Needs review » Needs work

Patch looks good, a few nitpicks.

  1. +++ b/core/lib/Drupal/Core/Path/AliasManager.php
    @@ -286,4 +286,18 @@ protected function pathAliasWhitelistRebuild($path = NULL) {
    +  /**
    +   * Wrapper method for REQUEST_TIME constant.
    +   *
    +   * @return int
    +   */
    +  protected function getRequestTime() {
    +    if (defined('REQUEST_TIME')) {
    +      return REQUEST_TIME;
    +    }
    +    else {
    +      return (int) $_SERVER['REQUEST_TIME'];
    +    }
    +  }
    

    Seeing that this method is used twice (here and in MemoryBackend), might it be useful to put this in a trait so it can be reused easily in new code that relies on REQUEST_TIME?

  2. +++ b/core/modules/migrate/tests/src/Unit/MigrateExecutableTest.php
    @@ -354,7 +354,7 @@ public function testTimeOptionExceeded() {
    -    $this->executable->limit = array('unit' => 'seconds', 'value' => (REQUEST_TIME - 3600));
    +    $this->executable->limit = array('unit' => 'seconds', 'value' => ((int) $_SERVER['REQUEST_TIME'] - 3600));
    

    The parentheses around the subtraction are not needed.

  3. +++ b/core/tests/Drupal/Tests/Core/Cache/ChainedFastBackendTest.php
    @@ -45,7 +45,7 @@ public function testGetDoesntHitConsistentBackend() {
         // Use REQUEST_TIME because that is what we will be comparing against.
    -    $timestamp_item = (object) array('cid' => $timestamp_cid, 'data' => REQUEST_TIME - 60);
    +    $timestamp_item = (object) array('cid' => $timestamp_cid, 'data' => (int) $_SERVER['REQUEST_TIME'] - 60);
    

    The line of documentation right above this still refers to the REQUEST_TIME constant.

  4. +++ b/core/tests/Drupal/Tests/Core/Path/AliasManagerTest.php
    @@ -207,7 +207,7 @@ public function testGetAliasByPathNoMatch() {
    -      ->with($this->cacheKey, array($language->getId() => array($path)), REQUEST_TIME + (60 * 60 * 24));
    +      ->with($this->cacheKey, array($language->getId() => array($path)), (int) $_SERVER['REQUEST_TIME'] + (60 * 60 * 24));
    

    These parentheses around the product are also not strictly needed, but this is OK as it helps with readability.

All unit tests are covered, we can be certain of this otherwise we would get failures with "Use of undefined constant REQUEST_TIME".

pfrenssen’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new13.24 KB
new1.79 KB

Fixed those little nitpicks. They don't change the actual functionality so I suppose I am still allowed to RTBC this.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 16: 2432343-16.patch, failed testing.

pfrenssen’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new22.31 KB

Oops I uploaded the wrong patch when I was in a hurry to catch my train. This is the patch I was looking for.

mpdonadio’s picture

Issue tags: +Needs change record

Does this need a CR to demonstrate the proper way to get this now in test and non-test code?

pfrenssen’s picture

Issue tags: -Needs change record

This is only introduced in 8.0.x but indeed we might want to inform developers about the change. Added draft change record.

jhedstrom’s picture

Status: Reviewed & tested by the community » Needs review

Seeing that this method is used twice (here and in MemoryBackend), might it be useful to put this in a trait so it can be reused easily in new code that relies on REQUEST_TIME?

This still isn't using a trait from what I can tell.

pfrenssen’s picture

Sorry I forgot to mention this, @alexpott told me on IRC that he didn't think it was necessary to make a trait for that simple method.

jhedstrom’s picture

Status: Needs review » Reviewed & tested by the community

Ah, back to RTBC then :)

hussainweb’s picture

+++ b/core/lib/Drupal/Core/Cache/MemoryBackend.php
@@ -202,6 +202,20 @@ public function removeBin() {
+  protected function getRequestTime() {
+    if (defined('REQUEST_TIME')) {
+      return REQUEST_TIME;
+    }
+    else {
+      return (int) $_SERVER['REQUEST_TIME'];
+    }
+  }
+

Do you think it is better to write this as:

return defined('REQUEST_TIME') ? REQUEST_TIME : (int) $_SERVER['REQUEST_TIME'];

It is not a blocker by any means, but I am thinking this is a quick change that saves a bunch of lines and makes the method easier to read.

hussainweb’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new1.17 KB
new22.21 KB

I didn't realize this was a Normal issue. I avoided putting it in a patch thinking not to block a critical. But this is a small change and makes the code easier to read, so here goes...

wim leers’s picture

Status: Needs review » Reviewed & tested by the community

Looks fine. I don't think it makes it easier to read; it's equally legible for me in case of such a simple function. So I think #18 & #25 are equivalent.

Back to RTBC.

pfrenssen’s picture

Priority: Normal » Major

I didn't realize this was a Normal issue. I avoided putting it in a patch thinking not to block a critical.

This is actually major since it blocks two major issues: #2232861: Create BrowserTestBase for web-testing on top of Mink and #2304461: KernelTestBaseTNG™.

dawehner’s picture

@hussainweb
It doesn't matter here, because we don't call it often, but in case you have code which is executed a lot of times, have a look at http://fabien.potencier.org/article/48/the-php-ternary-operator-fast-or-not

hussainweb’s picture

@dawehner: Thanks for the links. I did come across this long back but completely forgot about it.

It was really a nitpick. It is not worth holding up a major issue for this. It's back to RTBC so there is nothing to do. I am now leaning towards #18 even though it is more verbose but I am fine with either now.

webchick’s picture

Changes like this looks great:

-    $prepared->valid = $prepared->expire == Cache::PERMANENT || $prepared->expire >= REQUEST_TIME;
+    $prepared->valid = $prepared->expire == Cache::PERMANENT || $prepared->expire >= $this->getRequestTime();

Changes like this, not so much:

-      'updated' => REQUEST_TIME,
+      'updated' => (int) $_SERVER['REQUEST_TIME'],

we've tried really, really hard over the D8 development cycle to avoid direct consultation of globals, especially superglobals.

However, in talking about this on IRC with alexpott, neclimdul, EclipseGc and Crell, it doesn't actually make the "pre-existing condition" any worse. These tests were already consulting the superglobal directly, just doing so behind a constant that we had to jankily define.

So I think this is fine for now, but we should get a follow-up to (quoting Crell) "I would really like to see if we can get the request time injectable, because then any service can just take an int as a constructor argument and all is right with the world."

Searching https://www.drupal.org/list-changes/published?keywords_description=REQUE... however I don't really see anything that covers the fact that you're not supposed to use REQUEST_TIME anymore, and the change record only covers the case we actually don't generally want people to do. It probably needs to be re-jiggered into something more like "REQUEST_TIME constant removed" and laying out the two replacement paths ("recommended" way to inject the request object, and "if you have to" way (for tests only) to consult the superglobal directly).

Once that's done, this is fine to go in.

Crell’s picture

Angie asked me to weigh in here.

Overall, this issue is reducing coupling, even if only very slightly, so I'm OK with it. We're replacing an implicit dependency on the Drupal environment with one for the PHP environment, which is a step up even if still not ideal.

It would be useful to have a follow-up for the non-test code in here to go a step further and try to inject the request time as an integer dependency from the container, which would therefore decouple those services from the RequestStack. Problem is, the container doesn't support non-object services or runtime-generated parameters. However... it does support runtime object services, so we could instead of an int stick a DateTime into the container for "now" and inject that. That would have the same net decoupling result. That sounds like a good follow-up once this is committed.

neclimdul’s picture

I think the follow up is "as you unit test code, you can't use REQUEST_TIME anymore so things have to do something else"

webchick’s picture

Status: Reviewed & tested by the community » Fixed

So I thought I was waiting on a change record update mentioned in #30, but in grepping I see that it really is only unit tests where we no longer use REQUEST_TIME; it's still peppered throughout the code base in various places even with this patch (including KernelTestBase ones like /core/modules/system/src/Tests/Entity/EntityCrudHookTest.php).

So, sorry about the red herring.

Committed and pushed to 8.0.x. Thanks!

  • webchick committed 81a6979 on 8.0.x
    Issue #2459155 by alexpott, pfrenssen, hussainweb, neclimdul: Remove...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.