Enhancing types in PHP

Matthias Noback has published an article questioning whether DateTimeImmutable can be considered a primitive type. Matthias concludes that a DateTimeImmutable is not a primitive type.

Primitive type

We can probably agree that the following types

  • array
  • bool
  • float
  • int
  • object
  • string

are all primitive types.

Simple type

We can probably also agree that a DateTimeImmutable is better than a string, a non-empty string, and better than a string that matches a certain regular expression. So if you are dealing with a DateTimeImmutable, you can be sure it holds a valid date.

If you look at all the usages of a DateTimeImmutable in a code base, the only thing the corresponding values have in common is that they are valid dates. But are these values reasonable in a specific context? For example, do they refer to a date in the next quarter or a date of birth?

I am arguing that, at best, a DateTimeImmutable is a sophisticated primitive type, perhaps something we can call a simple type.

Ted M. Young calls these types unconstrained types.

Also see the slides for Ted's talk Stop Obsessing About Primitives.

Proper type

In my opinion, a proper type describes an object

  • that carries meaning (it's a date of birth)
  • that enforces constraints for the primitive(s) it encapsulates (it's today or in the past)
  • that encapsulates operations when necessary, for example, comparison with other instances (it refers to the same day)
  • that a developer can trace across a code base (all of these instances refer to dates of birth)

A DateTimeImmutable

  • does not carry meaning beyond referring to a date
  • can not enforce whether the encapsulated value is in the future or the past
  • does not encapsulate necessary operations, for example, comparison with other instances without knowing which components need to be considered
  • can not be reliably traced across a code base

Without context, a DateTimeImmutable object does not satisfy the requirements of a proper type.

From primitive to proper type

Replacing primitive types with simple types certainly enhances legacy code bases. However, it is only a step in the right direction while figuring out requirements.

For example, assume a legacy code base has a User.

<?php

declare(strict_types=1);

class User
{
    private string $dateOfBirth;

    // ...

    public function __construct(string $dateOfBirth)
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    public function dateOfBirth(): string
    {
        return $this->dateOfBirth;
    }

    // ...
}

As a first step, we can replace the primitive string with a DateTimeImmutable.

<?php

declare(strict_types=1);

class User
{
    private \DateTimeImmutable $dateOfBirth;

    // ...

    public function __construct(DateTimeImmutable $dateOfBirth)
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    public function dateOfBirth(): DateTimeImmutable
    {
        return $this->dateOfBirth;
    }

    // ...
}

As a second step, we can introduce a simple DateOfBirth type to replace the simple DateTimeImmutable.

<?php

declare(strict_types=1);

final class DateOfBirth
{
    private DateTimeImmutable $value;

    private function __construct(DateTimeImmutable $value)
    {
        $this->value = $value;
    }

    public static function fromDateTimeImmutable(DateTimeImmutable $value): self
    {
        return new self($value);
    }

    public function toString(): string
    {
        return $this->value;
    }
}

The simple DateOfBirth type

  • carries meaning (it's a date of birth)
  • encapsulates necessary operations, for example, comparison with other instances (it refers to the same day)
  • can be traced by a developer across a code base (all of these instances refer to dates of birth)

The simple DateOfBirth type does not yet enforce constraints for the simple type it encapsulates.

However, the User class and corresponding call sites can now already start using the DateOfBirth type.

<?php

declare(strict_types=1);

class User
{
    private DateOfBirth $dateOfBirth;

    // ...

    public function __construct(DateOfBirth $dateOfBirth)
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    public function name(): DateOfBirth
    {
        return $this->dateOfBirth;
    }

    // ...
}

As a third step, when requirements are clear, we can turn the simple DateOfBirth into a proper type that also enforces constraints for the simple DateTimeImmutable it encapsulates.

<?php

declare(strict_types=1);

final class DateOfBirth
{
    // ...

    /**
     * @throws InvalidDateOfBirth
     */
    public static function fromDateTimeImmutable(
        DateTimeImmutable $value,
        DateTimeImmutable $threshold
    ): self {
        if ($threshold->format('Y-m-d') < $value->format('Y-m-d')) {
            throw InvalidDateOfBirth::with($value);
        }

        return new self($value);
    }

    // ...
}

The proper DateOfBirth type now

  • carries meaning (it's a date of birth)
  • enforces constraints for the primitive(s) it encapsulates (it's today or in the past)
  • encapsulates comparison (it refers to the same day)
  • can be traced by a developer across a code base (all of these instances refer to dates of birth)

What do you think? Are you better off with primitive, simple, or proper types?

Do you find this article helpful?

Do you have feedback?

Do you need help with your PHP project?