Add a helper function: drupal_get_now()

Created on 29 October 2024, about 2 months ago

Problem/Motivation

Whenever I need to save a date+time programmatically, I write

$now = (new \Drupal\Core\Datetime\DrupalDateTime('now', new \DateTimeZone('UTC')))->format('Y-m-d\TH:i:s');
# or
$now = \Drupal::service('date.formatter')->format(time(), 'custom', 'Y-m-d\TH:i:s', 'UTC');

Even with the help of VS Code and/or a use statement, it's quite a lot to remember and rather complicated.

Imagine if I could just ask for drupal_get_now() and get all the goodness of (new \Drupal\Core\Datetime\DrupalDateTime('now', new \DateTimeZone('UTC')))->format('Y-m-d\TH:i:s') in one fell swoop!

Setting the date field on a node would be as easy as
$order_node->set('field_paid_date', drupal_get_now());

I bet

drupal_get_now('date'); // just the date
drupal_get_now('timestamp'); // timestamp

would be useful to lots of people as well.

Steps to reproduce

Try saving a date programmatically.

My guess is that 90% of the dates my sites save programmatically are 'now'.

Proposed resolution

Place the function in the datetime.module.

Remaining tasks

  1. Some discussion: should we make Drupal fast and friendly for developers, or should we bet on slow and opaque?
  2. Making the function

User interface changes

None.

Introduced terminology

None.

API changes

None.

Data model changes

None.

Feature request
Status

Active

Version

11.0 🔥

Component

datetime.module

Created by

🇩🇰Denmark Steven Snedker

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @Steven Snedker
  • Would this be better as a public static function on \Drupal\Core\Datetime\DrupalDateTime or similar than a global function, which this app's architecture is moving away from?

  • 🇩🇰Denmark Steven Snedker

    Claude is pretty keen on the idea:

    That's a great idea to simplify this common operation! Let me think through the architectural considerations:

    Public static method vs. global function:

    • Given Drupal's move away from global functions, a static method would indeed be more aligned with modern best practices
    • It would maintain better encapsulation and namespace organization
    • It follows object-oriented principles better than adding to the global namespace
    • It makes the functionality more discoverable through IDE tooling

    A static method could look something like:

    public static function nowUtc(string $format = 'Y-m-d\TH:i:s'): string 
    {
        return (new static('now', new \DateTimeZone('UTC')))->format($format);
    }

    Then you could use it as:

    $now = \Drupal\Core\Datetime\DrupalDateTime::nowUtc();

    This gives you:

    • A cleaner, more maintainable API
    • The ability to easily extend with additional formats or timezones if needed
    • Better consistency with modern PHP practices
    • Clear indication that this is Drupal-specific datetime functionality

    Would you like me to explore other potential approaches or elaborate on any aspects of this solution?

    Trading
    $now = (new \Drupal\Core\Datetime\DrupalDateTime('now', new \DateTimeZone('UTC')))->format('Y-m-d\TH:i:s');
    for
    $now = \Drupal\Core\Datetime\DrupalDateTime::nowUtc();
    is slightly better. You do not have to remember hyperspecific stuff like 'Y-m-d\TH:i:s'.

    Having it as a better service

    # Create a new service class
    class DateTimeService {
      public function getNowUtc(string $format = 'Y-m-d\TH:i:s'): string {
        return (new DrupalDateTime('now', new \DateTimeZone('UTC')))->format($format);
      }
    }

    would also trade very specific and human-unfriendly
    $now = \Drupal::service('date.formatter')->format(time(), 'custom', 'Y-m-d\TH:i:s', 'UTC');
    for better
    $now = \Drupal::service('datetime.service')->getNowUtc();

    I'm not sure if which of the public static function or the service will play nicest with IDE tooling. Which one would require the least remembering aand typing?

    My VS Code, filled to the brim with the right extensions still makes me pine for simple, global functions.

    $user = user_load(42);
    is still way easier than
    $user = \Drupal::entityTypeManager()->getStorage('user')->load(42);

  • 🇮🇳India nikhil_110

    It's a great idea to implement a global helper function to simplify this common operation. If @cilefen and @steven.snedker agree with this approach, I can prepare a patch for the helper function. Let me know your thoughts!
    Thanks

  • 🇩🇰Denmark Steven Snedker

    Well, my preference is

    1. drupal_get_now() (global function, not the modern Drupal way, will probably not make it into core)
    2. \Drupal\Core\Datetime\DrupalDateTime::now() (You'll almost be able to remember this one)
    3. \Drupal::service('datetime.service')->now() (harder to remember)

    Naming
    now is short and easy to remember. It's not as distinct as getNowUTC, but your IDE would probably tell you that now returned the date+time as UTC.

    Make a patch @nikhil_110 and set the ball in motion!

  • I don't think anyone will merge a new global scope function.

  • 🇮🇳India sourav_paul Kolkata

    Then @cilefen what could be the best approach?
    Please let us know, will work on that..

  • Merge request !10200Issue #3484259: Add a helper function: now() → (Open) created by sourav_paul
  • Pipeline finished with Failed
    about 1 month ago
    Total: 613s
    #339722
  • 🇮🇳India sourav_paul Kolkata

    I've test it locally, it works fine..

    SS:

  • 🇩🇰Denmark Steven Snedker

    Looks very fine to me. Short, elegant and to the point.

    Sadly I have no idea why we get "Merge request pipeline #339722 failed" at PHPUnit Functional 7/8 or how to get out of this regrettable situation.

  • 🇺🇸United States nicxvan

    If you click on it and search for fail you can find the failures.

    It's layout builder which is a known common failure.

    If you click get push access you can run that test group.

  • 🇺🇸United States smustgrave

    Still appears to need test coverage

  • 🇮🇳India sourav_paul Kolkata

    It would be appreciating, if anyone could write the test coverage.

  • 🇩🇰Denmark Steven Snedker

    I hear you, brother @sourav_paul. So I asked Claude to do it:

    I'll create a PHPUnit test for this static method from the DrupalDateTime class.

    This test class provides comprehensive coverage for the now() method with several test cases:

    testNowDefaultFormat(): Tests the default ISO 8601 format and verifies the timestamp is current
    testNowCustomFormat(): Tests various custom format strings using a data provider
    testNowTimezone(): Verifies that the method always uses UTC regardless of system timezone

    Key features of the tests:

    • Uses PHPUnit's data provider pattern to test multiple format strings
    • Includes regex pattern validation for different date formats
    • Handles timezone testing safely by restoring the original timezone
    • Includes proper PHPDoc blocks and type hints
    • Follows Drupal coding standards and testing conventions

    You can run these tests using PHPUnit from your Drupal root directory:
    ./vendor/bin/phpunit core/tests/Drupal/Tests/Core/Datetime/DrupalDateTimeTest.php

    And here's the test. I hope you can suss out where to put it:

    <?php
    
    namespace Drupal\Tests\Core\Datetime;
    
    use Drupal\Core\Datetime\DrupalDateTime;
    use Drupal\Tests\UnitTestCase;
    
    /**
     * @coversDefaultClass \Drupal\Core\Datetime\DrupalDateTime
     * @group DateTime
     */
    class DrupalDateTimeTest extends UnitTestCase {
    
      /**
       * Tests the default format of the now() method.
       *
       * @covers ::now
       */
      public function testNowDefaultFormat(): void {
        $datetime = DrupalDateTime::now();
        
        // Validate the format matches ISO 8601
        $this->assertMatchesRegularExpression(
          '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/',
          $datetime,
          'Default format should match ISO 8601 format (Y-m-d\TH:i:s)'
        );
        
        // Validate the timestamp is within 1 second of current time
        $now = time();
        $test_time = strtotime($datetime);
        $this->assertLessThanOrEqual(1, abs($now - $test_time), 
          'Generated timestamp should be within 1 second of current time'
        );
      }
    
      /**
       * Tests the now() method with custom formats.
       *
       * @covers ::now
       * @dataProvider provideTimeFormats
       */
      public function testNowCustomFormat(string $format, string $pattern): void {
        $datetime = DrupalDateTime::now($format);
        
        $this->assertMatchesRegularExpression(
          $pattern,
          $datetime,
          sprintf('Format %s should match pattern %s', $format, $pattern)
        );
      }
    
      /**
       * Data provider for testNowCustomFormat.
       *
       * @return array
       *   Test cases with format strings and their corresponding regex patterns.
       */
      public function provideTimeFormats(): array {
        return [
          'Year only' => [
            'Y',
            '/^\d{4}$/',
          ],
          'Date only' => [
            'Y-m-d',
            '/^\d{4}-\d{2}-\d{2}$/',
          ],
          'Time only' => [
            'H:i:s',
            '/^\d{2}:\d{2}:\d{2}$/',
          ],
          'Custom format' => [
            'd/m/Y H:i',
            '/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}$/',
          ],
          'Unix timestamp' => [
            'U',
            '/^\d+$/',
          ],
        ];
      }
    
      /**
       * Tests that the now() method uses UTC timezone.
       *
       * @covers ::now
       */
      public function testNowTimezone(): void {
        // Store current timezone
        $current_timezone = date_default_timezone_get();
        
        try {
          // Set system timezone to something other than UTC
          date_default_timezone_set('America/New_York');
          
          // Get timestamp in both timezones
          $utc_time = strtotime(DrupalDateTime::now('Y-m-d H:i:s'));
          $ny_time = strtotime((new \DateTime('now', new \DateTimeZone('America/New_York')))->format('Y-m-d H:i:s'));
          
          // Calculate the offset (should be 4 or 5 hours depending on daylight savings)
          $offset = abs($ny_time - $utc_time);
          
          $this->assertTrue(
            $offset >= 14400 && $offset <= 18000,
            'UTC time should differ from New York time by 4-5 hours'
          );
        }
        finally {
          // Restore original timezone
          date_default_timezone_set($current_timezone);
        }
      }
    
    }
  • First commit to issue fork.
  • Pipeline finished with Failed
    24 days ago
    Total: 864s
    #353101
  • Pipeline finished with Failed
    24 days ago
    Total: 670s
    #353126
  • 🇮🇳India shalini_jha

    I Have tried to add test coverage for now method.Moving this to NR , Kindly review & let me know if any updates are needed.

  • Pipeline finished with Success
    24 days ago
    Total: 5924s
    #353147
  • 🇷🇺Russia Chi

    There are a few concerns with the proposed approach.

    1. The implementation is hardcoded to one specific date format (Y-m-d\TH:i:s).
    2. It overlaps with PSR 20 where now() is supposed to return DateTimeImmutable object.
    3. It does not use core time service. So no way to mock the date in tests.

  • 🇳🇱Netherlands bbrala Netherlands

    As per #19 i'm setting this to needs work.

  • 🇩🇰Denmark Steven Snedker

    @chi, your concers addressed

    The implementation is hardcoded to one specific date format (Y-m-d\TH:i:s).

    Yeah. It looks shoddy but
    a) There is no core time format just like this
    b) That is the time format that all datetime fields expect.

    Can you improve it?

    It overlaps with PSR 20 where now() is supposed to return DateTimeImmutable object.

    Let's call this useful function getNow then.

    It does not use core time service. So no way to mock the date in tests.

    Hm. Is that a necessity?
    Can you point to (or have ChatGPT create) the specific tests @smustgrave wishes for?

  • 🇷🇺Russia Chi

    That is the time format that all datetime fields expect.

    Because it's storage format. It is rarely used in UI.
    You may make the method more flexible by adding optional format parameter like follows.

    function getNow(string $format = DateTimeItemInterface::DATETIME_STORAGE_FORMAT)
    
    Is that a necessity?

    It's a general reason for having services injected. Core uses @datetime service when needs to obtain current time.

    Hopefully, someday it'll switch from the custom baked TimeInterface to PSR-20.

  • 🇩🇰Denmark Steven Snedker

    As per #19 Add a helper function: drupal_get_now() Active and #20 Add a helper function: drupal_get_now() Active I give up.
    I will not spend any more time trying to port this enhancement correctly to Drupal core. It's just not worth the effort.
    May all your potential upcoming rewriting, pushing and requesting be met with success, @sourav_paul and @shalini_jha.

Production build 0.71.5 2024