Creating Strictly Typed Arrays and Collections in PHP

Share this article

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.

Frequently Asked Questions (FAQs) about Creating Strictly Typed Arrays and Collections in PHP

What are the benefits of using strictly typed arrays in PHP?

Strictly typed arrays in PHP provide a way to ensure that all elements in an array are of a specific type. This can be particularly useful in larger, more complex applications where data consistency is crucial. By enforcing a specific type for all elements in an array, you can prevent potential bugs and errors that might occur due to unexpected data types. It also makes your code more predictable and easier to debug, as you always know the type of data you’re working with.

How can I create a strictly typed array in PHP?

PHP does not natively support strictly typed arrays. However, you can create a class that enforces type checking on the elements added to the array. This class would have methods for adding and retrieving elements, and these methods would check the type of the element before performing the operation. If the type of the element does not match the expected type, an error would be thrown.

Can I use type hinting with arrays in PHP?

Yes, PHP supports type hinting for arrays. You can specify that a function or method expects an array as an argument by adding “array” before the argument name in the function or method declaration. However, this only ensures that the argument is an array, not that all elements in the array are of a specific type.

What is the difference between loosely typed and strictly typed arrays?

In a loosely typed array, the elements can be of any type. In a strictly typed array, all elements must be of a specific type. If you try to add an element of a different type to a strictly typed array, an error will be thrown.

How can I enforce type checking in PHP?

You can enforce type checking in PHP by using the “declare(strict_types=1);” directive at the beginning of your PHP file. This will enforce strict type checking for all function calls and return statements in the file.

Can I create a strictly typed array of objects in PHP?

Yes, you can create a strictly typed array of objects in PHP by creating a class that enforces type checking on the objects added to the array. The class would have methods for adding and retrieving objects, and these methods would check the type of the object before performing the operation.

What are the limitations of strictly typed arrays in PHP?

The main limitation of strictly typed arrays in PHP is that they require additional code to implement, as PHP does not natively support them. This can make your code more complex and harder to maintain. Additionally, strictly typed arrays can be less flexible than loosely typed arrays, as they do not allow for elements of different types.

Can I use type hinting with multidimensional arrays in PHP?

Yes, you can use type hinting with multidimensional arrays in PHP. However, PHP’s type hinting only ensures that the argument is an array, not that all elements in the array (or sub-arrays) are of a specific type.

How can I handle errors when using strictly typed arrays in PHP?

When using strictly typed arrays in PHP, you can handle errors by using try-catch blocks. If an error occurs when adding an element to the array (for example, if the element is of the wrong type), an exception will be thrown. You can catch this exception and handle it appropriately.

Can I use strictly typed arrays with PHP’s built-in array functions?

Yes, you can use strictly typed arrays with PHP’s built-in array functions. However, you need to be careful, as these functions do not enforce type checking. If you use a function that modifies the array and adds an element of the wrong type, this could lead to errors.

Bert RamakersBert Ramakers
View Author

Bert is a senior PHP developer living in Belgium and working at 2dotstwice (http://2dotstwice.be). While he started his career as a Drupal developer, his focus is now DDD and implementing REST API's with CQRS and Event Sourcing in micro-frameworks like Silex. In his spare time he likes to give back to the community by sharing knowledge in the form of articles, and by co-organizing PHP meetups.

arrayarraysBrunoScollectioncollectionsOOPHPPHPstatically typed
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week