Playing with PHP 8.6 Duration class
So when I was reading what will be added in PHP8.6, I noticed they added the Duration class. I saw potential of native support in the Apie library for this class, so I could not wait to test out this class and add support for it.
One important issue first: I wrote this article and did my experiment on august the 10th with the current PHP 8.6 development timeline. As of August 10, 2026, PHP 8.6 is still in pre-release development: PHP 8.6.0 Alpha 3 was released July 30, with Beta 1 planned for August 13.
What is the duration class?
The Duration class represents a duration with nanosecond precision.. The cool thing is that it's designed to not have a public constructor. That is a good design, because to me it makes very little sense when writing something like: new Duration(5): is this 5 seconds or 5 milliseconds?
A duration instance is created with a static create method:
use Time\Duration;
$duration = Duration::fromSeconds(5, 900_000_000); // 5.9 seconds
$duration = Duration::fromMilliseconds(5); // 5 milliseconds
It's design is not perfect to be honest. Why does fromSeconds have a second argument for the nanoseconds, but fromMilliseconds does not? And it is also weird I can not create a duration from a floating point. Looking at the current implementation, the value is represented internally using seconds, nanoseconds and a sign. The class is mutable, which surprised me somewhat. Given the existence of DateTimeImmutable and experiences with bugs with DateTime, I wonder whether an immutable duration type will eventually be useful as well.
Making a testcase
So I started with a simple test case without code changes that is skipped in PHP versions lower than 8.6:
namespace Apie\Tests\Core;
use Apie\Core\Context\ApieContext;
use Apie\Fixtures\TestHelpers\TestWithFaker;
use Apie\Fixtures\TestHelpers\TestWithOpenapiSchema;
use Apie\Serializer\Serializer;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Time\Duration;
#[RequiresPhp('>=8.6')]
class Php86Test extends TestCase
{
use TestWithFaker;
use TestWithOpenapiSchema;
#[Test]
#[DataProvider('provideFromSeconds')]
public function it_can_be_denormalized(mixed $expected, mixed $input): void
{
$serializer = Serializer::create();
$actual = $serializer->denormalizeNewObject($input, Duration::class, new ApieContext());
static::assertEquals($expected, $actual);
}
public static function provideFromSeconds(): array
{
return [
[Duration::fromMilliseconds(20), 20],
[Duration::fromMilliseconds(20), 20.5],
];
}
#[Test]
public function it_works_with_schema_generator()
{
$this->runOpenapiSchemaTestForCreation(
Duration::class,
'Duration-post',
[
'type' => 'number'
]
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function it_works_with_apie_faker()
{
$this->runFakerTest(Duration::class);
}
}
Class Time\Duration not found!
FROM debian:trixie
RUN apt-get update && apt-get install -y \
autoconf \
build-essential \
bison \
pkg-config \
re2c \
libxml2-dev \
libsqlite3-dev \
libcurl4-openssl-dev \
libonig-dev \
libzip-dev \
git \
&& git clone --depth=1 https://github.com/php/php-src.git /usr/src/php \
&& cd /usr/src/php \
&& ./buildconf \
&& ./configure --enable-mbstring --with-openssl \
&& make -j"$(nproc)" \
&& make install
CMD ["php", "-v"]
The most important part was to include mbstring as phpunit requires this. And as expected: all the tests fail, because my library can not handle objects with a private constructor out of the box.
json_encode a duration
While some people already have made simple polyfills for adding support for the Duration class in older PHP versions, I wanted to know how it behaves in actual PHP 8.6. For example: what happens if I use json_encode on it:
I was expecting a floating point number or an integer. If you want the floating point 1,5 to be converted into Duration::fromSeconds(1, 500_000_000); you have to do this yourself.
I used the structure given by json_encode for apie/serializer into serializing a Duration object to JSON. In a POST or PUT request body I do allow support for denormalizing a number into milliseconds or follow the internal structure currently used by PHP 8.6. If PHP 8.6 changes it, my testcase will fail here, which is intended as it is still very early to know if this becomes the used structure:
namespace Apie\Serializer\Normalizers;
use Apie\Core\Lists\ItemHashmap;
use Apie\Core\Lists\ItemList;
use Apie\Serializer\Context\ApieSerializerContext;
use Apie\Serializer\Interfaces\NormalizerInterface;
use Apie\TypeConverter\ReflectionTypeFactory;
use Psr\Http\Message\UploadedFileInterface;
use Time\Duration;
class DurationNormalizer implements NormalizerInterface
{
public function supportsNormalization(mixed $object, ApieSerializerContext $apieSerializerContext): bool
{
return $object instanceof Duration;
}
public function supportsDenormalization(string|int|float|bool|null|ItemList|ItemHashmap|UploadedFileInterface $object, string $desiredType, ApieSerializerContext $apieSerializerContext): bool
{
return $desiredType === Duration::class;
}
public function normalize(mixed $object, ApieSerializerContext $apieSerializerContext): ItemHashmap
{
return $apieSerializerContext->normalizeAgain(json_decode(json_encode($object), true));
}
}For example for apie/faker this became my faker class to add support for Duration:
namespace Apie\Faker\Fakers;
use Apie\Faker\Interfaces\ApieClassFaker;
use Faker\Generator;
use ReflectionClass;
use Time\Duration;
/** @implements ApieClassFaker<Duration> */
class DurationFaker implements ApieClassFaker
{
public function supports(ReflectionClass $class): bool
{
return $class->name === Duration::class;
}
public function fakeFor(Generator $generator, ReflectionClass $class): Duration
{
return Duration::fromSeconds($generator->numberBetween(0, 1000), $generator->numberBetween(0, 999999999));
}
}
Conclusion
As for Duration: it does what it should do and my Apie library should support it natively. I do find the internal datastructure with seconds and nanoseconds and a negative boolean a bit odd but it does what it should do. The internal error control is also greatly appreciated. I could not enter invalid data.
I do wonder if the json_encode behaviour is specified or intentional behaviour, but again it works.

Comments
Post a Comment