PHP's array
is a powerful datatype. You can add and remove values of any type at any time. This comes at a cost though: You have no control over what is allowed and what not. This can become a problem in your domain code where you need to define very strict rules about your domain. If you pass an array
of User
objects into a method, is it allowed to contain users who are inactive? If you want the array to be sorted (the user with the latest registration date for example), you cannot assume that the calling code has already done that.
But there is a solution to this problem: You can create a class which implements the \Traversable
interface of PHP. The class needs to implement a getIterator(): Traversable
method now, so you have full control over how it's elements are handled by external code. Encapsulating your iterables into their own classes might seem to bloat your code at first. But while this approach indeed comes with additional code, it has some major advantages.
Advantages
Internal type safety
When you encapsulate the concept of a list of User
objects into it's own class, you can easily make sure to only allow User
objects to be part of the list. Throw an exception if a different type is passed into it. Now you never have to do any type checks in the code that uses your iterable object.
Constrained access options
You have full control over which methods you add to your class. You don't have to worry that elements of your object are modified in a way you did not intend. Yes, you need to write those methods by yourself, but this gives you the power to decide for each individual case what is needed and what not.
External type safety
If your code gets an array
passed in, you don't really know what you get. You need to look at the calling code and hope that it does not change. But if you get a custom UserList
for example, you know exactly what it's possibilities and restrictions are.
Encapsulation of business logic
Often you need to perform the same operation on a list of objects multiple times, for example filter down a list of all users to only those with an active account. This code tends to go into service classes or - even worse - into controllers and other application layer code. Encapsulating these business rules into your iterable classes makes them easy to test and reuse. The calling code is also very straightforward to read: $users->filterOnlyActive()
.
Immutability
Writing your own methods enables you to always return a new object instead of modifying the existing one. This way you don't have to worry to break existing code when working with an iterable object.
Sorting
In an array
you don't have information about it's sorting. While you can sort an array
you need to do it directly in the code that uses it to make sure it wasn't modified elsewhere. By creating a dedicated class for a sorted list (e.g. SortedUserByAgeList
) which makes sure the elements are always in the correct order you don't need to worry about it in the calling code any more.
Disadvantages
More Code
This one is pretty straightforward: You need to write those classes. If you don't have a complex domain model and you don't intend your application to be maintained for a long time, this might be overkill. But as soon as these classes exist, the improved readability and safety kicks in.
Performance
This one is more critical. The additional type checks and method calls make the execution of the code slower than using simple arrays. If this is a problem or not simply comes down to the use case and needs some profiling. Don't just dismiss the idea because of premature optimization.
Summary
I've been using this approach in multiple projects over the last few years and it made my code much more readable and safe. I use it mainly in my domain layer where safety is a must and I found that I could simplify the calling code in many places by moving business logic into my iterable classes.
In my first approaches I used a base class that provided a type check in the constructor and all the common methods for filtering, mapping, counting, etc. But this caused a huge inheritance tree which in turn caused many methods to exist only for a few subclasses and made adjustments very hard. Then I shifted the concept to a simple class that provides the common functionality but is instead used by the custom classes. This way I can decide for every custom class which methods to expose and just forward the method calls to the underlying generic one. Very specific methods are then handled by the custom classes on their own.
Now that I've come to a point where the structure of the generic classes do not change that much any more I extracted them into their own library: https://packagist.org/packages/cunningsoft/generic-list.