profile picture

Typed arrays in PHP

An alternative to the missing feature in PHP: Generics

October 13, 2020 - 653 words - 4 mins Found a typo? Edit me
software php array generics

blog-cover

Argument unpacking, function variable argument list, and variadics function.

The perfect combination

We will use this snipped for our examples Having a class, Customer:

<?php
/** 
 * @psalm-immutable 
 */
final class Customer
{
    // Using PHP 8 constructor property promotion
    // https://wiki.php.net/rfc/constructor_promotion
    public function __construct(
        public string $name,
    ) {}
}
// We create a list of 6 customers
$customers = array_map(
    fn(int $i): Customer => new Customer("name-{$i}"),
    range(1, 6)
);

Whenever we want to manipulate a list of Customers, we can pass as an argument: …$customers.

How we used to do it

We define the array type using the PHPDoc param comment block above. But we cannot define the real type of the item. The code will still run without any problem passing any type on that argument array $customers:

<?php
/** 
 * @param Customer[] 
 */
function createInvoiceForCustomers(array $customers): void
{
    foreach ($customers as $customer) {
        // ... some irrelevant logic for this example
    }
}

The code below would work at “compile-time”. But it might fail at “runtime”.

<?php
createInvoiceForCustomers($customers);
createInvoiceForCustomers([new Customer('any name')]);
createInvoiceForCustomers([new AnyOtherType()]);

An alternative (recommended!) might be to extract that logic and ask for the particular type in order to “check it” at runtime in that particular moment, failing if one of the items wasn’t really a Customer:

<?php
/** 
 * @param Customer[] 
 */
function createInvoiceForCustomers(array $customers): void
{
    foreach ($customers as $customer) {
        createInvoice($customer);
    }
}
function createInvoice(Customer $customer): void
{
    // ... some irrelevant logic for this example
}

Everything here below would work at “compile-time”. It will for sure break during “runtime” if the createInvoice(Customer $customer) receives something different than a Customer.

<?php
createInvoiceForCustomers($customers);
createInvoiceForCustomers([new Customer('any name')]);
createInvoiceForCustomers([new AnyOtherType()]); // won't work

By doing that createInvoice(Customer $customer) we are ensuring the type of the argument, which is good! But, what about going one step further. Could we check the types of the elements when calling the function createInvoiceForCustomers(array $customers), even making the IDE complain when the types are not right?

Well, that’s actually what Generics are for, but sadly, they are not yet in PHP. Not even in the upcoming PHP 8. Hopefully in a near future, but we cannot predict that for now. Luckily, we have currently an alternative nowadays, but it’s not that popular. It has its own “pros” and “cons”, so let’s take a look at an example first:

<?php
function createInvoiceForCustomers(Customer ...$customers): void
{
    foreach ($customers as $customer) {
        createInvoice($customer);
    }
}

Everything here below would work at “compile-time”. It will for sure break during “runtime” if the createInvoice() receives something different than a Customer.

<?php
createInvoiceForCustomers(...$customers); // OK
createInvoiceForCustomers(
    new Customer('any name'), 
    new Customer('any name'),
); // OK
// This is not even possible to write. The IDE will yeld at you. 
// It's expecting a `Customer`, but `AnyOtherType` is given:
createInvoiceForCustomers(new AnyOtherType());

PROS

CONS

Important remarks

Conclusions

Argument unpacking is a great feature that, in combination with variadic functions, can help us to simulate typed arrays. With great power comes great responsibility, and this is no exception. We need to learn about our toolbox in order to use it wisely.

blog-cover


References