Collection objects in PHP
Tags: php, collection, typesafety, oop
Published: 2018-09-14 14:30:38

---
title: Collection objects in PHP
published: true
description: One of the most important things to know about PHP is that it's loosely typed. Whereas in some cases this might offer you a plus of flexibility, it is often a problem. But, thankfully, there are ways to work around this implemented in the language. In this article, you'll learn how to build type-safe collection objects behaving exactly as arrays would.
tags: PHP, collection, typesafety, oop
---

# The problem with arrays

In PHP arrays are a very flexible data structure, which accepts almost everything and can be combined any way you like. This is actually good: unlike other languages, PHP lets you even combine associative and numeric arrays, thus offering you a great deal of freedom. But there are times when you just *need* type-safety.

One way to deal with it would surely be to check every item of the array when using it, thus creating some code snippets close to this:

```php
$array = [1,2,3,4,5]

foreach ($array as $item) {
    if (!is_int($item)) {
        throw new \Exception('wrong type');
    }

    //do your stuff here
}
```

While this obviously works, it's a bit quirky:

* in order to factorize all occurrences, you need some sort of globally accessible function
* it is, in fact, the wrong place to check it. Ideally, you would want to check the type of a value when creating the array, and don't bother when using it because you are sure that it's ok.

# The solution: collections

A collection is a class that abstracts the array for you. A basic collection class would look like this:

```php
class IntCollection {
    private $values = [];

    public function addValue (int $val, $key = null): void
    {
        //the typehint ensures that you won't have any non-integer values
        if ($key === null) {
            $this->values[] = $val;
        } else {
            $this->values[$key] = $val;
        }
    }

    public function deleteValue($key): void
    {
        unset($this->values[$key]);
    }

    public function get($key): int
    {
        return $this->values[$key];
    }
}
```

That's already pretty cool, but not enough: you can't use it as an array. This means for instance that
* If you are refactoring, chances are that you have to change multiple code parts in order to replace an array with this object
* you can't count the items in it
* you can't use it in standard PHP control structures like foreach

# The better solution

Luckily, PHP provides us with the tools to make all of this possible. For this, we need to refactor our collection class in order to use some of the core languages interfaces.

An interface is an API definition for a class. When implementing an interface, you tell the world that your class will work a certain, standardized and predefined way. When you look at it the first time, it may strike you as "just like an abstract class", but it's not quite right:

* all methods declared in an interface must be public
* abstract classes can contain variables and implemented methods, interfaces can't
* a class can only extend one other class, but may implement as many interfaces as you like

In order to build our array-like, type-safe collection, we'll look at three PHP interfaces

* [Countable](http://php.net/manual/en/class.countable.php) => Tells the system your class can be used in functions like 'count()'
* [Iterator](http://php.net/manual/en/class.iterator.php) => Tells the system that your object can be iterated through
* [ArrayAccess](http://php.net/manual/en/class.arrayaccess.php) => implement this and you'll be able to use your collection like an array (for instance accessing it via $collection[$key];)

Our finished IntCollection would look like this:

```php
class IntCollection implements \Countable, \Iterator, \ArrayAccess
{

    private $values = [];
    private $position = 0;

    /**
     * This constructor is there in order to be able to create a collection with
     * its values already added
     */
    public function __construct(array $values = [])
    {
        foreach ($values as $value) {
            $this->offsetSet('', $value);
        }
    }

    /**
     * Implementation of method declared in \Countable.
     * Provides support for count()
     */
    public function count()
    {
        return count($this->values);
    }

    /**
     * Implementation of method declared in \Iterator
     * Resets the internal cursor to the beginning of the array
     */
    public function rewind()
    {
        $this->position = 0;
    }

    /**
     * Implementation of method declared in \Iterator
     * Used to get the current key (as for instance in a foreach()-structure
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * Implementation of method declared in \Iterator
     * Used to get the value at the current cursor position
     */
    public function current()
    {
        return $this->values[$this->position];
    }

    /**
     * Implementation of method declared in \Iterator
     * Used to move the cursor to the next position
     */
    public function next()
    {
        $this->position++;
    }

    /**
     * Implementation of method declared in \Iterator
     * Checks if the current cursor position is valid
     */
    public function valid()
    {
        return isset($this->values[$this->position]);
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used to be able to use functions like isset()
     */
    public function offsetExists($offset)
    {
        return isset($this->values[$offset]);
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used for direct access array-like ($collection[$offset]);
     */
    public function offsetGet($offset)
    {
        return $this->values[$offset];
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used for direct setting of values
     */
    public function offsetSet($offset, $value)
    {
        if (!is_int($value)) {
            throw new \InvalidArgumentException("Must be an int");
        }

        if (empty($offset)) { //this happens when you do $collection[] = 1;
            $this->values[] = $value;
        } else {
            $this->values[$offset] = $value;
        }
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used for unset()
     */
    public function offsetUnset($offset)
    {
        unset($this->values[$offset]);
    }
}
```

# An even better solution: using the SPL

The [SPL](http://php.net/manual/en/book.spl.php) is a core library from PHP in which you'll find many very useful things if you bother looking. Unfortunately, it is almost as widely unknown as it is useful. For our problem here, it contains a class called ArrayObject, which does everything we did by hand before and already is serializable. By extending the object, our final collection class will be written in literally no time:

```php
class IntCollection extends \ArrayObject
{
    public function offsetSet($index, $newval)
    {
        if (!is_int($newval)) {
            throw new \InvalidArgumentException("Must be int");
        }

        parent::offsetSet($index, $newval);
    }
}
```