whateverthing.com

PHP: Fixing Deprecated strftime() calls

Over the years, I've accumulated a lot of small PHP projects that are in varying stages of their maintenance lifecycle. Sometimes they feel like overgrown cars lying in a fallow yard, wheels gone, propped up on blocks, forgotten. I'm getting around to improving them, but occasionally I hit roadblocks.

Right now, a tiny but annoying roadblock is PHP 8.1's deprecation of the strftime() function. One of my Whateverthing projects uses it a handful of times in various ways. I need to find an alternative pattern to replace it, one which is either brief and low-effort, or more robust but available to all the sections of the codebase.

Three high-volume calls exist within a single method. That particular method is overdue for a rewrite. It "fixes" datetime strings so they are displayed in the user's preferred timezone. This codebase's database, like many built on the west coast of North America, shipped with "Pacific Time" as the default. Localizing datetimes for users, then, requires a multi-step shuffle: first to UTC or GMT, and then to the user's own timezone.

... or does it?

After a couple of hours of bashing my head against timezone math, I decided to go back to the drawing board and see how I'd implement this if I were building it from square one. And boy howdy did I find what I was looking for.

Because - guess what - it turns out that this is a solved problem.

I was able to rip out all my dodgy strftime()-backed timezone math and replace it with this:

$originalDate = new DateTimeImmutable($date, new DateTimeZone($originalTz));

$newDate = $originalDate->setTimezone(new DateTimeZone($newTz));

There might be an even shorter way to write it, but for now this'll do. And it lets me delete at least 12 lines of useless broken code. Yay!

For future reference, the list of possible timezones can be generated by calling DateTimeZone::listIdentifiers(). Note that spaces are replaced with underscores, so the New York timezone is America/New_York.

The moral of this story is, if something changes underfoot, don't get stuck trying to make things work the old way. Rethink.

... er, just one more thing ...

Oh, but the story's not over. I also need to format the strings - after all, that was strftime()'s main job.

The downside here is that the strings used by strftime() are not compatible with DateTimeImmutable's format() method. But, there's a helpful reference table on the PHP website, and I only had two strings I needed to translate. Well, three, if you count the sane way of showing a datetime value.

Format Example Old Format String New Format String
Dec 5, 01:14 %b %e, %H:%M M j, H:i
Dec 5 '22, 01:14 %b %e '%y, %H:%M M j 'y, H:i
2022-12-05 01:14:00 %Y-%m-%d %H:%M:%S Y-m-d H:i:s

There are some sneaky differences here, such as "%M" becoming "i". The old syntax is documented under strftime, and the new syntax is documented at datetime.format on the ever-helpful PHP website.

... and a closing tip

I've wrapped this up in a set of classes I call "Shift" and "Format", in a namespace called "Timelord". I was expecting the timezone shifting logic to be more complex than this, and thought I might need to write a custom format mapper from strftime() to ->format() syntax. I'm very glad it was unnecessary, but I like the name, so I'm keeping it. Maybe I'll have more adventures in date math to add to it in the future.

One benefit of this choice is that it let me add short PHPUnit test cases that "broke" my original dodgy math. Once I ripped all that out and replaced it with the call to ->setTimezone(), I was able to have some degree of confidence that I had actually fixed the problems:

class ShiftTest extends TestCase
{
    /**
     * @dataProvider timezoneProvider
     */
    public function testShift(
        $currentTime, 
        $expectedFriendlyTime, 
        $fromTimeZone, 
        $toTimeZone
    ): void {
        $overrideTimeZone = new \DateTimeZone($fromTimeZone);
        $overrideTime = new \DateTimeImmutable($currentTime, $overrideTimeZone);

        $shift = new Shift();
        $shift->overrideTime($overrideTime);
        $shift->overrideTimeZone($overrideTimeZone);

        $dateString = $currentTime;

        $actual = $shift->fix($dateString, $toTimeZone);

        $format = new Format();

        $this->assertSame($expectedFriendlyTime, $format->friendly($actual));
    }

    public function timezoneProvider()
    {
        yield 'pacific-to-eastern' => [
            '2022-02-02 02:02:02',
            'Feb 2, 05:02',
            'America/Vancouver',
            'America/New_York',
        ];
    }

    public function testShift_Now_timezone_override(): void
    {
        $shift = new Shift();
        $shift->overrideTimeZone(new \DateTimeZone('America/Mexico_City'));

        $this->assertSame(
            'America/Mexico_City',
            $shift->now()->getTimezone()->getName()
        );
    }
}

Yes, that's right, the Shift class also implements the newly-ratified PSR-20 standard.

Thanks for reading! I hope you found this helpful and informative. Happy upgrading!

Published: December 5, 2022

Categories: coding

Tags: php, howto