PHP 7: Type-safe Arrays of Objects

With PHP 7 you can choose to write much more type-safe code than before, thanks to scalar type hints and return types.

function repeat(string $text, int $times) : string;

But what about arrays? There’s still only the generic “array” type hint, you cannot specify what’s in the array. For the IDE, you can add PhpDoc comments:

/**
 * @return User[]
 */
function allUsers() : array;

Now IDEs like PhpStorm can help with code completion for items in the returned array. But we cannot benefit from any checks at runtime, like with real type hints.

For arguments, there is a partial workaround, using variadic arguments. Take the following function

/**
 * @param User[] $users
 */
function deleteUsers(array $users);

With variadic arguments we can rewrite it to

function deleteUsers(User ...$users);

Usage also changes, to deleteUsers(...$users); In this call, the argument $users will be “unpacked” into single variables, and in the method itself “packed” back into an array $users. Each item is validated to be of type User. $users can also be an iterator, it will be converted to an array.

Unfortunately there is no similar workaround for return types, and it only works for the last argument.

See also: Type hinting in PHP 7 – array of objects

I’ve used this technique a lot in PHP 7 code, but I’ve found another one that’s even better and does not come with the mentioned flaws:

Collection objects

Every time I need an array of objects, I create a class instead. Here’s a simple example:

class Users extends ArrayIterator
{
    public function __construct(User ...$users)
    {
        parent::__construct($users);
    }
    public function current() : User
    {
        return parent::current();
    }
    public function offsetGet($offset) : User
    {
        return parent::offsetGet($offset);
    }
}

This collection extends ArrayIterator, so that it can be used almost like an array: iterating with foreach and accessing elements with []. Array functions cannot be used but with iterator_to_array() it can be converted to a normal array.

Note that you can only change return type hints to be more restrictive than in the parent class. So overriding offsetSet would not work and the object will be in an unvalidated state until each item is accessed.

If we don’t need the ArrayAccess interface to access elements with [], there’s a solution for that: encapsulate the array iterator and only expose what you need:

class Users extends IteratorIterator
{
    public function __construct(User ...$users)
    {
        parent::__construct(new ArrayIterator($users));
    }
    public function current() : User
    {
        return parent::current();
    }
}

(did you ever wonder, why IteratorIterator exists? Here I found a use case!)

This Users object can only be created with User elements, thanks to the variadic constructor arguments. Passing anything else to the constructor will result in a catchable TypeError.

new Users($user1, $user2, $user3);

And it’s even immutable, it cannot be changed after instantiation. But if we need to change it, we now can add type-safe methods for that, like:

public function add(User $user)
{
    $this->getInnerIterator()->append($user);
}
public function set(int $key, User $user)
{
    $this->getInnerIterator()->offsetSet($key, $user);
}

You can also make it countable (because we know, the inner iterator implements Countable):

    public function count() : int
    {
        return $this->getInnerIterator()->count();
    }

Array functions

As mentioned before, if you need array functions, you can always convert the object to an array with iterator_to_array(). But there’s another solution which often makes more sense: move the function into the collection class. For sorting functions it’s easy because they are already part of ArrayIterator, so we can do it the same way as with count() above.

But let’s take another example and merge two user collections:

public function merge(Users $other)
{
    return new Users(
        array_merge(
            iterator_to_array($this),
            iterator_to_array($other)
        )
    );
}

Now it can be used as

$allUsers = $goodUsers->merge($badUsers);

When adding methods to the collection objects, be as specific as possible to move logic into the collection that belongs there. For example, instead of creating a generic uasort() method, create exactly the sort method(s) that you need:

public function sortByAge()
{
    $this->getInnerIterator()->uasort(
        function(User $a, User $b) {
            return $a->age() <=> $b->age();
        }
    );
}

That makes the client code much clearer (something that would not have been possible with arrays):

$users->sortByAge()

With filter/map/reduce methods, we also have the option to work with collection pipelines, as I explained in a previous blog post: Collection Pipelines in PHP

Domain Logic

When I need a collection of objects, I often create a simple class first, extending IteratorIterator, like in the example above. The obvious advantage is type safety, but it also makes it much easier to add domain logic in the right place later. As soon as I have methods that operate on a collection of objects, I can add them directly to the collection class, where they naturally belong to. Otherwise they might end up in some utility or helper class (Stop using Helpers!), or even in the client code that uses the collection.

Consider this part of a card game:

class AwesomeCardGame
{
  /** @var Card[] */
  private $stock;
  /** @var Card[] */
  private $pile;
  /**
   * shuffle discard pile and put back to stock
   */
  public function turnPile()
  {
    $this->stock = $this->pile
    $this->pile = [];
    $this->turnAllCards($this->stock);
    shuffle($this->stock);
  }
  private function turnAllCards(array $cards)
  {
    foreach ($cards as $card) {
      $card->turn();
    }
  }
}

It mixes high level game logic with low level implementation details. With a card collection object we can move methods that operate on the cards there, and keep the Game class at one abstraction level:

class AwesomeCardGame
{
  /** @var Cards */
  private $stock;
  /** @var Cards */
  private $pile;
  /**
   * shuffle discard pile and put back to stock
   */
  public function turnPile()
  {
    $this->pile->moveTo($this->stock);
    $this->stock->turnAll();
    $this->stock->shuffle();
  }
}

Interfaces

It’s often useful to define the collection as interface first. It extends the Iterator interface and contains at least the current() method with specified return type.

Minimum example:

interface Users extends \Iterator
{
  public function current() : User;
}

With the default implementation as wrapped ArrayIterator:

final class UsersArray implements Users extends \IteratorIterator
{
  public function __construct(User ...$users)
  {
    parent::__construct(new ArrayIterator($users));
  }
  public function current() : User
  {
    return parent::current();
  }
}

Why is it useful?

Exchangeable implementation

First, if your code is going to be used by third parties, they will be able to swap out the implementation. My favorite example are lazy loading collections with generators for better performance with large collections:

final class LazyLoadingUsers implements Users extends \IteratorIterator
{
  public function __construct(Generator $generator)
  {
    parent::__construct($generator);
  }
  public function current() : User
  {
    return parent::current();
  }
}

which then can be instantiated like this:

$userGenerator = function(PDOStatement $stmt) {
  while ($user = $stmt->fetchObject(User::class)) {
    yield $user;
  }
}
new LazyLoadingUsers(
  $userGenerator($pdo->query('SELECT * FROM users'))
);

Here the User objects are not fetched from the database before you actually iterate over the lazy loading collection. Since generators can only be iterated once, the collection won’t keep all objects in memory at once.

Extensibility with decorators

Second, you will be able to extend the class with decorators, that wrap the original instance and implement the same interface, but add additional behavior. You can even use the collection to decorate all items on the fly.

Let’s say the default Card implementation in our awesome card game has a __toString() method that returns card suit and rank as plain text, and added a decorator for a HTML representation:

final class HtmlCard implements Card
{
  /** @var Card */
  private $card;
  public function __construct(Card $card)
  {
    $this->card = $card;
  }

  public function __toString()
  {
    $plain = parent::__toString();
    return sprintf(
      '<img src="%s" alt="%s" />',
      $this->imageUrl($plain)
      $plain
    );
  }
  private function imageUrl(string $plain) : string
  {
    // ...
  }
}

Now we can write a decorator for the collection as well, that takes any Cards collection and decorates its elements with HtmlCard:

final class HtmlCards implements Cards
{
  /** @var Cards */
  private $cards;
  public function __construct(Cards $cards)
  {
    $this->cards = $cards;
  }
  public function current() : Card
  {
    return new HtmlCard(parent::current());
  }
  public function next() { $this->cards->next(); }
  public function rewind() { $this->cards->rewind(); }
  public function valid() { return $this->cards->valid(); }
  public function key() { return $this->cards->key(); }
}

Conclusion

That was lots of code. To wrap it up:

  • Variadic arguments can be used to type hint a single array argument
  • Implementing basic collection objects as array replacement is very simple with IteratorIterator
  • You can convert them with iterator_to_array(), but it’s better to move array methods into the collection instead
  • Once you have collection objects, they naturally attract business logic, as you will see that they are the right place for methods that operate on its elements
  • With interfaces, you gain flexibility. For examply you can replace array based collections with generator based collections or add decorators
  • You can do more cool stuff with collections, see: Collection pipelines

Image: Roo Reynolds CC-BY-NC 2.0

11 Replies to “PHP 7: Type-safe Arrays of Objects”

  1. One of my own criticisms of PHP now it has type safety (so since 2015) is the productivity burden of such code, where in languages like Java and C# you can write a user class, then have the simple `user[] someListOfUsers` as an argument or variable.

    Another problem of this that nobody seems to be talking about is the fact PHP arrays are not arrays at all, but rather some hashmap (can’t think of anyone else doing the same), but I’d suppose that to be a topic for another day.

  2. Nice article :).

    PHP really needs generics… That way you could simply have a type safe collection without all the boilerplate (a new class for each entity for which you want to use it).

    By the way, can’t you make the constructor private and add static methods like (little bit coloured naming here) `UsersCollection::safe(User …$users)` and `UsersCollection::unsafe(Traversable $traversable)`? Then you can construct a `UsersCollection` from any iterator whatsover, specifically generators. Makes thing very composable.

    And if you let `UsersCollection` extend a general `Collection` class, then you could easily write generic helper functions like `Collection::filter(callable $f)` and `Collection::take(int $n)`. For instance:

    “`
    public function filter(callable $f): Collection
    {
    return new static(new CallbackFilterIterator($this, $f));
    }
    “`

    By using `new static`, you’d return actually a `UsersCollection` if you call `$users->filter($f)`.

    Thanks

    1. Thanks for your suggestions! As you already noticed in the gist, as soon as you extend any SPL iterator, you cannot make the constructor private (I tried that too). I like the “safe” and “unsafe” naming, though. And yeah, to add generic methods like filter to a base class makes sense if you want to use them. Of course this would be a perfect use case for generics, but it doesn’t look like we’ll get them anytime soon.

  3. I find arrays quite bad itself in performance and memory consumption. Now I prefer using own data structures with own binary tree indexes.

    Also prefer having using auto-generated contracts based on interface, don’t like a lot of boilerplate to write myself.

    Maybe in feature we have generics (templates) built into PHP some day.

    1. For improved performance, did you try php-ds? Vector, Set etc. all implement Traversable, so they could also be wrapped by IteratorIterator instead of using the ArrayIterator as described here. A code generator might be interesting here, but I prefer something like PhpStorm templates or a command line tool instead of magic at runtime.

    1. Because it’s needlessly more complex than __construct(User ...$users) and I’m fine with new Users($user1, $user2) instead of new Users([$user1, $user2]).

      Or am I missing the point of “just”? What’s the benefit?

    1. First of all, type safety, as mentioned. None of the SPL data structures and not even the new DS datastructures provide that, so you can use them as internal data structure (i.e. inner iterator), but not as a replacement.

  4. The idea is nice. But currently it’s not possible to pass more than one variadic variable in a constructor.

    This is not working.

    `<?php

    class Comment {}
    class User {}

    class SomeLogic
    {
    public function __construct(User …$users, Commment …$commments)
    {

    }
    }`

    What we need is a type safe array in PHP.

Leave a Reply

Your email address will not be published. Required fields are marked *