DEV Community

Vincent Fahrenholz
Vincent Fahrenholz

Posted on • Edited on

Collection objects in PHP

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:

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

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

    //do your stuff here
}
Enter fullscreen mode Exit fullscreen mode

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:

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];
    }
}
Enter fullscreen mode Exit fullscreen mode

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 => Tells the system your class can be used in functions like 'count()'
  • Iterator => Tells the system that your object can be iterated through
  • ArrayAccess => 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:

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]);
    }
}
Enter fullscreen mode Exit fullscreen mode

An even better solution: using the SPL

The SPL 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:

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

        parent::offsetSet($index, $newval);
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (8)

Collapse
 
sebastienbarre profile image
Sebastien Barre

Guys. Maybe don't do that? Use strict typing and the splat/spread operator, as shown here, so you can add or more values, and you get type checking exceptions for free:
medium.com/2dotstwice-connecting-t...

Collapse
 
aleksikauppila profile image
Aleksi Kauppila • Edited

As you mentioned arrays are very flexible and offer freedom for developers. It's what makes them good but also very annoying to consume. Personally i feel that only arrays containing a single type is acceptable as return value (see my post on the subject). Iterators are better in most cases.

To be honest i really don't think it's important to implement ArrayAccess on collections (or extend ArrayObject). It's not really bad... but somehow using that bracket syntax feels awkward.

Lack of collections is a pain in the ass in PHP. There is really no convenient way to achieve type safety for collections at the moment (at least one i know of). I create collections all the time by just implementing IteratorAggregate but this means that i do have to create a new class each time i want type safety for a collection.

I really hope we'll see generics sooner rather than later. Also sets and maps.

Collapse
 
anwar_nairi profile image
Anwar • Edited

I've also seen a work in progress around built-ins collections : php.net/manual/fr/class.ds-collect..., might be the next step over Sets, Maps and others useful collections like the one Python provide.

Collapse
 
fahrenholz profile image
Vincent Fahrenholz

I've seen it also, but it is a pecl extension (working with PHP 7). I doubt it will become core soon.

Collapse
 
anwar_nairi profile image
Anwar

Too bad, I feel PHP diserve those built-ins, kind of like they are doing for strictly typed variables...

Collapse
 
rotexdegba profile image
Rotimi Ade

Guys, check out a collection package I have been working on since may 2018

github.com/rotexsoft/versatile-col...

It is fully documented and it supports strict typing. It has a lot of the laravel collection features and more.

Cheers!

Collapse
 
mzhasan2016 profile image
mzhasan2016

Really awesome article.

Collapse
 
stilldreaming1 profile image
still-dreaming-1

"literally"?