I have been working on an interesting project. This project requires me to work with an API that gives back sometimes unexpected results.

Of course, my goal is to have completely predictable code that has known behavior. If I can't rely on my code, then I can't do much else can I? I can't help that my data source is unreliable, but perhaps I can do something to make sure their unreliability does not interfere with my code.

This got me into a line of thinking. If I make a query that is expected to return a list of a given object, how do I enforce that the list contains nothing except for that specific object? You might be familiar with seeing something like this:

/**
 * @return DesiredObject[]
 */
function someQuery(): array
{
    // ...
}

This is pretty helpful. We are enforcing that the return type must be an array, and the doc mentions that it will be a list of DesiredObject's so our IDE can type hint for us. What if the code looked like this though?

/**
 * @return DesiredObject[]
 */
function someQuery(): array
{
    return [
        new DesiredObject(),
        new DesiredObject(),
        new \DateTime()
    ];
}

That will be technically valid as the someQuery() method did indeed return an array, but we do not know that all the items are what we expect. Of course you could make a check during result iteration to make sure, but that can be slow and tedious. What we want to do is create our own iterator that does type enforcement within PHP for the array contents. Lucky for us, PHP has a handy iterator interface we can use. In this case we don't actually want to write out all those method stubs so we can use the IteratorIterator class to get us started. We can extend this class to a class that will act as a type enforcing "collection," like so:

final class DesiredObjectIterator extends IteratorIterator
{
    public function current(): DesiredObject
    {
        return parent::current();
    }
}

Looking good so far! We are type enforcing all the elements upon return. We can feed in any Traversable item to its constructor and use it as a normal iterator. In this example ill use the ArrayIterator just to get the ball moving.

/**
 * @return DesiredObjectIterator
 */
function someQuery(): DesiredObjectIterator
{
    return new DesiredObjectIterator(
        new ArrayIterator([
            // An array of the items we are returning
        ])
    );
}

There is a pretty significant problem with that though. Type enforcement is only done during iteration, meaning we can create the iterator with invalid data. We don't want that, we do not want to be able to create the collection object with invalid data at all! To do this we can use PHPs type enforcement combined with the "splat operator", which was introduced in PHP 5.6.

final class DesiredObjectIterator extends IteratorIterator
{
    public function __construct(DesiredObject ...$desiredObjects)
    {
        parent::__construct(
            new ArrayIterator($desiredObjects)
        );
    }
    public function current(): DesiredObject
    {
        return parent::current();
    }
}

Using this new collection class, we can update our query function like so:

/**
 * @return DesiredObjectIterator
 */
function someQuery(): DesiredObjectIterator
{
    $results = [
        // However we got the results
    ];
    return new DesiredObjectIterator(...$results);
}

Now, because we are doing type enforcement both going in and out, we are stopping bad data from entering our application as early as possible with minimum overhead!

An unfortunate downside to this is that we cannot do dynamic type enforcement while maintaining the native PHP strong-type system. You could write your own type checking system of course, but that would add some overhead. This means that you must create a new collection object for each type of object you want to "collect." In my application I found myself with a list of 5-6 various collection types.

To wrap up, lets have an engagement challenge. Get back to me on Twitter or Mastodon or wherever and lets talk about this. Did this help you in a project? Do you think is is dumb? Let me know!