PHP
Article
By Bert Ramakers

Creating Strictly Typed Arrays and Collections in PHP

By Bert Ramakers

This post first appeared on Medium and was republished here with the author’s permission. We encourage you to follow Bert on Medium and give him some likes there!


One of the language features announced back in PHP 5.6 was the addition of the ... token to denote that a function or method accepts a variable length of arguments.

Something I rarely see mentioned is that it’s possible to combine this feature with type hints to essentially create typed arrays.

For example, we could have a Movie class with a method to set an array of air dates that only accepts DateTimeImmutable objects:

<?php

class Movie {  
  private $dates = [];

  public function setAirDates(\DateTimeImmutable ...$dates) {
    $this->dates = $dates;
  }

  public function getAirDates() {
    return $this->dates;
  }
}

We can now pass a variable number of separate DateTimeImmutable objects to the setAirDates() method:

<?php

$movie = new Movie();

$movie->setAirDates(
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-01-28'),
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-02-22')
);

If we were to pass something else than a DateTimeImmutable, a string for example, a fatal error would be thrown:

Catchable fatal error: Argument 1 passed to Movie::setAirDates() must be an instance of DateTimeImmutable, string given.

If we instead already had an array of DateTimeImmutable objects that we wanted to pass to setAirDates(), we could again use the ... token, but this time to unpack them:

<?php

$dates = [
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-01-28'),
  \DateTimeImmutable::createFromFormat('Y-m-d', '2017-02-22'),
];

$movie = new Movie();
$movie->setAirDates(...$dates);

If the array were to contain a value that is not of the expected type, we would still get the fatal error mentioned earlier.

Additionally, we can use scalar types the same way starting from PHP 7. For example, we can add a method to set a list of ratings as floats on our Movie class:

<?php

declare(strict_types=1);

class Movie {
  private $dates = [];
  private $ratings = [];

  public function setAirDates(\DateTimeImmutable ...$dates) { /* ... */ }
  public function getAirDates() : array { /* ... */ }

  public function setRatings(float ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverageRating() : float {
    if (empty($this->ratings)) {
      return 0;
    }

    $total = 0;

    foreach ($this->ratings as $rating) {
      $total += $rating;
    }

    return $total / count($this->ratings);
  }
}

Again, this ensures that the ratings property will always contain floats without us having to loop over all the contents to validate them. So now we can easily do some math operations on them in getAverageRating(), without having to worry about invalid types.

Problems with This Kind of Typed Arrays

One of the downsides of using this feature as typed arrays is that we can only define one such array per method. Let’s say we wanted to have a Movie class that expects a list of air dates together with a list of ratings in the constructor, instead of setting them later via optional methods. This would be impossible with the method used above.

Another problem is that when using PHP 7, the return types of our get() methods would still have to be “array”, which is often too generic.

Solution: Collection Classes

To fix both problems, we can simply inject our typed arrays inside so-called “collection” classes. This also improves our separation of concerns, because we can now move the calculation method for the average rating to the relevant collection class:

<?php

declare(strict_types=1);

class Ratings {
  private $ratings;

  public function __construct(float ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverage() : float {
    if (empty($this->ratings)) {
      return 0;
    }

    $total = 0;

    foreach ($this->ratings as $rating) {
      $total += $rating;
    }

    return $total / count($this->ratings);
  }
}

Notice how we’re still using a list of typed arguments with a variable length in our constructor, which saves us the trouble of looping over each rating to check its type.

If we wanted the ability to use this collection class in foreach loops, we’d simply have to implement the IteratorAggregate interface:

<?php

declare(strict_types=1);

class Ratings implements IteratorAggregate {
  private $ratings;

  public function __construct(float ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverage() : float { /* ... */ }

  public function getIterator() {
     return new ArrayIterator($this->ratings);
  }
}

Moving on, we can also create a collection for our list of air dates:

<?php

class AirDates implements IteratorAggregate {
  private $dates;

  public function __construct(\DateTimeImmutable ...$dates) {
    $this->dates = $dates;
  }

  public function getIterator() {
     return new ArrayIterator($this->airdates);
  }
}

Putting all the pieces of the puzzle together in the Movie class, we can now inject two separately typed collections in our constructor. Additionally we can define more specific return types than “array” on our get methods:

<?php

class Movie {
  private $dates;
  private $ratings;

  public function __construct(AirDates $airdates, Ratings $ratings) {
    $this->airdates = $airdates;
    $this->ratings = $ratings;
  }

  public function getAirDates() : AirDates {
    return $this->airdates;
  }

  public function getRatings() : Ratings {
    return $this->ratings;
  }
}

Using Value Objects for Custom Validation

If we wanted to add extra validation to our ratings we could still go one step further, and define a Rating value object with some custom constraints. For example, a rating could be limited between 0 and 5:

<?php

declare(strict_types=1);

class Rating {
  private $value;

  public function __construct(float $value) {
    if ($value < 0 || $value > 5) {
      throw new \InvalidArgumentException('A rating should always be a number between 0 and 5!');
    }

    $this->value = $value;
  }

  public function getValue() : float {
    return $this->value;
  }
}

Back in our Ratings collection class, we would only have to do some minor alterations to use these value objects instead of floats:

<?php

class Ratings implements IteratorAggregate {
  private $ratings;

  public function __construct(Rating ...$ratings) {
    $this->ratings = $ratings;
  }

  public function getAverage() : Rating {
    if (empty($this->ratings)) {
      return new Rating(0);
    }

    $total = 0;

    foreach ($this->ratings as $rating) {
      $total += $rating->getValue();
    }

    $average = $total / count($this->ratings);
    return new Rating($average);
  }

  public function getIterator() { /* ... */ }
}

This way we get additional validation of individual collection members, still without having to loop over each injected object.

Advantages

Typing out these separate collection classes and value object may seem like a lot of work, but they have several advantages over generic arrays and scalar values:

  • Easy type validation in one place. We never have to manually loop over an array to validate the types of our collection members;

  • Wherever we use these collections and value objects in our application, we know that their values have always been validated upon construction. For example, any Rating will always be between 0 and 5;

  • We can easily add custom logic per collection and/or value object. For example the getAverage() method, which we can re-use throughout our whole application;

  • We get the possibility to inject multiple typed lists in a single function or method, which we cannot do using the ... token without injecting the values in collection classes first;

  • There are significantly reduced odds of mixing up arguments in method signatures. For example, when we want to inject both a list of ratings and a list of air dates, the two could easily get mixed up by accident upon construction when using generic arrays;

What about edits?

By now you might be wondering how you could make changes to the values of your collections and value objects after initial construction.

While we could add methods to facilitate edits, this would quickly become cumbersome because we would have to duplicate most methods on each collection to keep the advantage of type hints. For example, an add() method on Ratings should only accept a Rating object, while an add() method on AirDates should only accept a DateTimeImmutable object. This makes interfacing and/or re-use of these methods very hard.

Instead, we could simply keep our collections and value objects immutable, and convert them to their primitive types when we need to make changes. After we’re done making changes, we can simple re-construct any necessary collections or value objects with the updated values. Upon (re-)construction all types would be validated again, along with any extra validation we might have defined.

For example, we could add a simple toArray() method to our collections, and make changes like this:

<?php

// Initial Ratings collection
$ratings = new Ratings(
  new Rating(1.5),
  new Rating(3.5),
  new Rating(2.5)
);

// Convert the collection to an array.
$ratingsArray = $ratings->toArray();

// Remove the 2nd rating.
unset($ratingsArray[1]);
$ratingsArray = array_values($ratingsArray);

// Back to a (new) Ratings collection
$updatedRatings = new Ratings(...$ratingsArray);

This way we can also re-use existing array functionality like array_filter().

If we really needed to do edits on the collection objects themselves, we could add the necessary methods on a need-to-have basis wherever they are required. But keep in mind that most of those will also have to do type validation of the given argument(s), so it’s hard to re-use them across all different collection classes.

Re-Using Generic Methods

As you may have noticed we are still getting some code duplication across our collection classes by implementing both toArray() and getIterator() on all of them. Luckily these methods are generic enough to move to a generic parent class, as they both simply return the injected array:

<?php

abstract class GenericCollection implements IteratorAggregate
{
  protected $values;

  public function toArray() : array {
    return $this->values;
  }

  public function getIterator() {
    return new ArrayIterator($this->values);
  }
}

All we would be left with in our collection class would be type validation in the constructor, and any optional extra logic that is specific to that collection, like this:

<?php

class Ratings extends GenericCollection
{
  public function __construct(Rating ...$ratings) {
    $this->values = $ratings;
  }

  public function getAverage() : Rating { /* ... */ }
}

Optionally we could make our collection final, to prevent any child classes from messing with the values property in ways that could undo our type validation.

Conclusion

While still far from perfect, it has steadily been getting easier to work with type validation in collections and value objects with recent releases of PHP.

Ideally we’d get some form of generics in a future version of PHP to further facilitate the creation of re-usable collection classes.

A feature that would greatly improve the usage of value objects would be the ability to cast an object to different primitive types, in addition to string. This could easily be implemented by adding extra magic methods comparable to __toString(), like __toInt(), __toFloat(), etc.

Luckily there are some RFCs in progress to possibly implement both features in later versions, so fingers crossed! 🤞


If you found this tutorial helpful, please visit the original post on Medium and give it some ❤️. If you have any feedback, questions, or comments, please leave them below or as a response on the original post.

  • vladimir prieto

    the fatal error image (the stylus table), is just a representation of the error or it is a real output error of some library/plugin?

    • Bruno Škvorc

      It’s just xdebug

  • good article.

  • Piotrooo

    Hi,

    Good article, but I found probably small error – you should create an instance of ArrayIterator before return, like this:
    return new ArrayIterator($this->dates);

    • Bert Ramakers

      You are correct, thanks! I’ll see if I can get that fixed.

    • Bruno Škvorc

      Thank you, fixed

  • Well done

Recommended
Sponsors
Get the latest in PHP, once a week, for free.