Reinforce the type safety of your PHP arrays
Tags: webdev, programming, php
Published: 2023-08-13 20:28:08

---
title: Reinforce the type safety of your PHP arrays
published: true
description: Arrays are not enforcing all items to be of the same type. With this technique, we can counter this issue.
tags: webdev, programming, php
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5ytx6gdm97rzzv8adw2.jpg
---

Welcome to this post where I will show you how to bring more type safety to your arrays in PHP.

## Summary

- [What is the problem?](#what-is-the-problem)
- [Typing our items](#typing our items)
- [Using objects as type safe array](#using-objects-as-type-safe-array)
- [Conclusion](#conclusion)

## What is the problem?

In my experience it is very rare to have list of multiple type of data within an array. Most of the time, we want to have a list of something. 

```php
use App\Services\User;

$cards = (new User())->getSavedCreditCards();

print_r($cards);
```

```
[
  [
    "id" => "YTZERT37RR3TR7",
    "type" => "amex",
    "lastFour" => "0826",
    "expiry" => [
      "month" => "04",
      "year" => "2024",
    ],
  ],
  [
    "id" => "83E38Y38YF8FYF3",
    "type" => "visa",
    "lastFour" => "1162",
    "expiry" => [
      "month" => "02",
      "year" => "2023"
    ],
  ],
]
```

Let us imagine the `UserService` takes data from our configured payment service provider of choice, and this is actually the raw response that is returned on the `UserService::getSavedCreditCards()`.

There is several problem having to deal with this kind of response:

- You are exposed to "undefined array key..." errors
- Your IDE cannot help you know what is the key name (if you will have to go on the source code to find the array shape, if you are lucky and it is not burried inside the PSP vendor code)
- It is not guaranteed the array will only contain a list of stored cards

## Typing our items

In this example objects would help convey more information in the code and bring type safety. 

Let us start with a basic object representing a credit card.

```php
readonly class Card
{
  public function __construct(
    public string $id,
    public CardType $type,
    public string $lastFour,
    public CardExpiry $expiry,
  ) {}
}

readonly class CardExpiry
{
  public function __construct(
    public string $month,
    public string $year,
  ) {}
}

enum CardType: string
{
  case AmericanExpress = "amex";
  case Visa = "visa";
  case MasterCard = "mastercard";
}
```

Now we could stop there and create an array of `Card`.

```php
$cards = [
  new Card(
    id: "YTZERT37RR3TR7",
    type: CardType::AmericanExpress,
    lastFour: "0826",
    expiry: new CardExpiry(
      month: "04",
      year: "2024",
    ),
  ),
  new Card(
    id: "83E38Y38YF8FYF3",
    type: CardType::Visa,
    lastFour: "1162",
    expiry: new CardExpiry(
      month: "02",
      year: "2023",
    ),
  ),
];
```

At this point, nothing prevents us from adding something else than a `Card` in this array. It is time to introduce "array objects".

## Using objects as type safe array

Let us start with a class that holds a list of `Card`.

```php
class Cards
{
  private array $cards;

  public function __construct()
  {
    $this->cards = [];
  }
}
```

To be able to push values to an object as if it was an array, let us implement the `ArrayAccess` interface.

```php
class Cards implements ArrayAccess
{
  private array $cards;

  public function __construct()
  {
    $this->cards = [];
  }

  // Array access methods
  public function offsetExists(mixed $offset): bool
  {
    return isset($this->cards[$offset]);
  }

  public function offsetGet($offset): ?Card
  {
    return $this->cards[$offset] ?? null;
  }
  
  public function offsetSet(mixed $offset, mixed $value): void
  {
    if (!$value instanceof Card) {
      throw new InvalidArgumentException("Expected parameter 2 to be a Card.");
    }
  
    $this->cards[$offset] = $value;
  }
  
  public function offsetUnset($offset): void 
  {
    if (isset($this->cards[$offset])) {
      unset($this->cards[$offset]);
    }
  }
}
```

Now we can safely push `Card` into the array.

```php
$cards = new Cards();

$cards[] = new Card(
  id: "YTZERT37RR3TR7",
  type: CardType::AmericanExpress,
  lastFour: "0826",
  expiry: new CardExpiry(
    month: "04",
    year: "2024",
  ),
);

$cards[] = 1; // Uncaught InvalidArgumentException: Expected parameter 2 to be a Card.
```

To be able to loop through the array, we need to implement another built-in interface.

```php
class Cards implements ArrayAccess, IteratorAggregate
{
  private array $cards;

  public function __construct()
  {
    $this->cards = [];
  }

  // Iterator aggregate methods
  public function getIterator(): ArrayIterator
  {
    return new ArrayIterator($this->cards);
  }

  // Array access methods
  public function offsetExists(mixed $offset): bool
  {
    return isset($this->cards[$offset]);
  }

  public function offsetGet($offset): ?Card
  {
    return $this->cards[$offset] ?? null;
  }
  
  public function offsetSet(mixed $offset, mixed $value): void
  {
    if (!$value instanceof Card) {
      throw new InvalidArgumentException("Expected parameter 2 to be a Card.");
    }
  
    $this->cards[$offset] = $value;
  }
  
  public function offsetUnset($offset): void 
  {
    if (isset($this->cards[$offset])) {
      unset($this->cards[$offset]);
    }
  }
}
```

And now we can loop through our cards.

```php
$cards = new Cards();

$cards[] = new Card(
  id: "YTZERT37RR3TR7",
  type: CardType::AmericanExpress,
  lastFour: "0826",
  expiry: new CardExpiry(
    month: "04",
    year: "2024",
  ),
);

foreach ($cards as $card) {
  echo $card->expiry->month;  // "04"
}
```

## Conclusion

This "array object" is the equivalent of a "Set" in other language: it ensures the list have a consistent type across all its items. 

One downside is that this check is done at runtime (unless you keep reading). The PHP interpreter will not natively understand your `$cards` array is a list of `Card`. Your IDE will still consider `$card` as a `mixed`. The last trick is to use the `assert()` function to give an extra help to your IDE.

```php
foreach ($cards as $card) {
  assert($card instanceof Card);

  echo $card->expiry->month;  // "04", IDE autocompletion works
}
```

Static analysis tools would prevent you from assigning an `int` to the lis of `Card`, with some extra PHPDoc blocks.

```php
/**
 * @implements ArrayAccess<int, Card>
 * @implements IteratorAggregate<int, Card>
 */
class Cards implements ArrayAccess, IteratorAggregate
{
  // ...
}

$cards = new Cards();

$cards[] = 1; // PHPStan will prevent you from doing this now
```

You can try what PHPStan tells you online with this [pre-filled example](https://phpstan.org/r/ba7b92bd-8a98-48a9-ad02-452d05ea5c0e).

Now you can ensure type consistency across your lists, which gives an extra layer of safety, and can be a great ally when working in teams.

At some point I hope this article will be deprecated in favor of generics within PHP. A proposal already thrown the basis for something like this.

```php
$cards = array<Card>();

$cards[] = new Card(...);
$cards[] = 1; // Both PHPstan and PHP would throw
```

In the mean time, I hope you will leave with some ideas to make your code base more robust. Thank you for reading!

Happy type hardening 🛡️

Cover image by [Pixabay](https://www.pexels.com/photo/silver-knight-helmet-289831/).