Home > Backend Development > PHP Tutorial > Creating Strictly Typed Arrays and Collections in PHP

Creating Strictly Typed Arrays and Collections in PHP

Christopher Nolan
Release: 2025-02-10 11:20:11
Original
131 people have browsed it

Creating Strictly Typed Arrays and Collections in PHP

Key Takeaways

  • PHP 5.6 introduced the ability to create typed arrays using the … token, which denotes that a function or method accepts a variable length of arguments. This feature can be combined with type hints to ensure that only certain types of objects are accepted in an array.
  • One limitation of this feature is that only one typed array can be defined per method. To overcome this, typed arrays can be injected into “collection” classes, which also allows for more specific return types than “array” on get methods.
  • Value objects can be used for custom validation. For instance, a Rating value object could be created with constraints to ensure that a rating is always between 0 and 5. This provides additional validation of individual collection members without having to loop over each injected object.
  • Strictly typed arrays and collections have several advantages. They provide easy type validation in one place, ensure values have always been validated upon construction, allow for the addition of custom logic per collection, and reduce the odds of mixing up arguments in method signatures.
  • While it’s possible to add methods to facilitate edits to the values of collections and value objects after initial construction, it’s more efficient to keep them immutable and convert them to their primitive types when changes need to be made. After making changes, the collections or value objects can be reconstructed with the updated values, which will then be validated again.

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:

<span><span><?php
</span></span><span>
</span><span><span>class Movie {  
</span></span><span>  <span>private $dates = [];
</span></span><span>
</span><span>  <span>public function setAirDates(\DateTimeImmutable ...$dates) {
</span></span><span>    <span>$this->dates = $dates;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getAirDates() {
</span></span><span>    <span>return $this->dates;
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login
Copy after login
Copy after login
Copy after login

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

<span><span><?php
</span></span><span>
</span><span><span>$movie = new Movie();
</span></span><span>
</span><span><span>$movie->setAirDates(
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-01-28'),
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-02-22')
</span></span><span><span>);
</span></span>
Copy after login
Copy after login
Copy after login

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

Creating Strictly Typed Arrays and Collections in PHP

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:

<span><span><?php
</span></span><span>
</span><span><span>$dates = [
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-01-28'),
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-02-22'),
</span></span><span><span>];
</span></span><span>
</span><span><span>$movie = new Movie();
</span></span><span><span>$movie->setAirDates(...$dates);
</span></span>
Copy after login
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>class Movie {  
</span></span><span>  <span>private $dates = [];
</span></span><span>
</span><span>  <span>public function setAirDates(\DateTimeImmutable ...$dates) {
</span></span><span>    <span>$this->dates = $dates;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getAirDates() {
</span></span><span>    <span>return $this->dates;
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login
Copy after login
Copy after login
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>$movie = new Movie();
</span></span><span>
</span><span><span>$movie->setAirDates(
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-01-28'),
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-02-22')
</span></span><span><span>);
</span></span>
Copy after login
Copy after login
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>$dates = [
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-01-28'),
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-02-22'),
</span></span><span><span>];
</span></span><span>
</span><span><span>$movie = new Movie();
</span></span><span><span>$movie->setAirDates(...$dates);
</span></span>
Copy after login
Copy after login

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

<span><span><?php
</span></span><span>
</span><span><span>declare(strict_types=1);
</span></span><span>
</span><span><span>class Movie {
</span></span><span>  <span>private $dates = [];
</span></span><span>  <span>private $ratings = [];
</span></span><span>
</span><span>  <span>public function setAirDates(\DateTimeImmutable ...$dates) { /* ... */ }
</span></span><span>  <span>public function getAirDates() : array { /* ... */ }
</span></span><span>
</span><span>  <span>public function setRatings(float ...$ratings) {
</span></span><span>    <span>$this->ratings = $ratings;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getAverageRating() : float {
</span></span><span>    <span>if (empty($this->ratings)) {
</span></span><span>      <span>return 0;
</span></span><span>    <span>}
</span></span><span>
</span><span>    <span>$total = 0;
</span></span><span>
</span><span>    <span>foreach ($this->ratings as $rating) {
</span></span><span>      <span>$total += $rating;
</span></span><span>    <span>}
</span></span><span>
</span><span>    <span>return $total / count($this->ratings);
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>declare(strict_types=1);
</span></span><span>
</span><span><span>class Ratings {
</span></span><span>  <span>private $ratings;
</span></span><span>
</span><span>  <span>public function __construct(float ...$ratings) {
</span></span><span>    <span>$this->ratings = $ratings;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getAverage() : float {
</span></span><span>    <span>if (empty($this->ratings)) {
</span></span><span>      <span>return 0;
</span></span><span>    <span>}
</span></span><span>
</span><span>    <span>$total = 0;
</span></span><span>
</span><span>    <span>foreach ($this->ratings as $rating) {
</span></span><span>      <span>$total += $rating;
</span></span><span>    <span>}
</span></span><span>
</span><span>    <span>return $total / count($this->ratings);
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>declare(strict_types=1);
</span></span><span>
</span><span><span>class Ratings implements IteratorAggregate {
</span></span><span>  <span>private $ratings;
</span></span><span>
</span><span>  <span>public function __construct(float ...$ratings) {
</span></span><span>    <span>$this->ratings = $ratings;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getAverage() : float { /* ... */ }
</span></span><span>
</span><span>  <span>public function getIterator() {
</span></span><span>     <span>return new ArrayIterator($this->ratings);
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login

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

<span><span><?php
</span></span><span>
</span><span><span>class AirDates implements IteratorAggregate {
</span></span><span>  <span>private $dates;
</span></span><span>
</span><span>  <span>public function __construct(\DateTimeImmutable ...$dates) {
</span></span><span>    <span>$this->dates = $dates;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getIterator() {
</span></span><span>     <span>return new ArrayIterator($this->airdates);
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>class Movie {  
</span></span><span>  <span>private $dates = [];
</span></span><span>
</span><span>  <span>public function setAirDates(\DateTimeImmutable ...$dates) {
</span></span><span>    <span>$this->dates = $dates;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getAirDates() {
</span></span><span>    <span>return $this->dates;
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login
Copy after login
Copy after login
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>$movie = new Movie();
</span></span><span>
</span><span><span>$movie->setAirDates(
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-01-28'),
</span></span><span>  <span><span>\DateTimeImmutable</span>::createFromFormat('Y-m-d', '2017-02-22')
</span></span><span><span>);
</span></span>
Copy after login
Copy after login
Copy after login

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:

<span><span><?php
</span></span><span>
</span><span><span>class Movie {  
</span></span><span>  <span>private $dates = [];
</span></span><span>
</span><span>  <span>public function setAirDates(\DateTimeImmutable ...$dates) {
</span></span><span>    <span>$this->dates = $dates;
</span></span><span>  <span>}
</span></span><span>
</span><span>  <span>public function getAirDates() {
</span></span><span>    <span>return $this->dates;
</span></span><span>  <span>}
</span></span><span><span>}
</span></span>
Copy after login
Copy after login
Copy after login
Copy after login

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! ?

  • Generics: https://wiki.php.net/rfc/generics

  • Generic arrays: https://wiki.php.net/rfc/generic-arrays

  • Casting object to scalar: https://wiki.php.net/rfc/class_casting_to_scalar


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.

The above is the detailed content of Creating Strictly Typed Arrays and Collections in PHP. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template