cult3

Creating a PHP Shopping Basket Part 2 - Working with Collections

Jan 28, 2015

Table of contents:

  1. What is a Collection class?
  2. Why use a Collection class?
  3. Why not use an Open Source Collection class?
  4. The Collection class
  5. Implementing the Countable interface
  6. Implementing the IteratorAggregate interface
  7. The methods of a Collection
  8. Conclusion

PHP has a number of basic building blocks that allow you to build powerful websites and web applications. If you’re a long time reader of Culttt, I’m going to assume you are already pretty familiar with variables, arrays and objects.

Arrays are a data construct that allow you to group together and manipulate a number of individual items. There are also a number of array specific native PHP functions for working with arrays.

However, in the world of Object Oriented PHP Programming, there is a better solution. Collection classes allow you to work with multiple items, but with all the benefits of OOP.

In today’s article we’re going to be looking at the benefits of using Collection classes and why you would want to use them over plain old arrays.

What is a Collection class?

Before I get into the reasoning behind using Collection classes over arrays I think it’s important that we are all on the same page when it comes to their usage.

Traditionally in PHP when working with multiple items, you might group them together in an array, like this:

$colours = ["read", "blue", "green"];

You could then manipulate the array using one of PHP’s native functions:

$count = count($colours);

foreach ($colours as $colour) {
    // Do something
}

Using a Collection class is basically the same as using an array, but instead of using PHP’s native functions, we can use the object’s methods instead:

$colours = new Collection(["red", "blue", "green"]);

$count = $colours->count();

$colours->each(function ($colour) {
    // Do something
});

Why use a Collection class?

As you can see from the examples above, the Collection class has all of the functionality of an array combined with the native PHP array functions.

So why would you want to use a Collection class over plain old native arrays?

I think there are basically two main reasons as to why you would want to use Collection classes.

Firstly, one of the main benefits of Object Oriented Programming is the fact that the object encapsulates logic internally so that the outside world does not need to be concerned with how the logic is implemented.

When you use a Collection class, you don’t need to use the correct PHP function to manipulate the items of the array because you can simply use the public API of the class.

So for example if you wanted to get the last item of an array, normally you would write:

$item = end($items);

However, if you were using a Collection class, you could simply write:

$item = $items->last();

Secondly, an object will often need to hold a list of internal items. For example, if we had a Basket object, that object would need to a hold a collection of Product objects.

If we were to use a native array to hold the Product objects, the logic for counting the number of products in the basket would leak out:

count($basket->products);

Alternatively, we could implement the logic on the Basket object:

$basket->count();

However, in this example we’re leaking logic and forcing functionality onto the wrong object. The Basket should not be concerned with being able to count the number of objects in the product list.

This problem can be solved by instantiating an internal instance of Collection and using it as the product list. The logic for manipulating the collection is encapsulated so it does not leak out into other objects or the outside world.

$basket->products->count();

With the logic to manipulate collections of items abstracted to an object, we can now reuse this basic object anywhere in our code. The object does not have any dependencies and so you can freely instantiate it inside another object to replace native arrays.

Why not use an Open Source Collection class?

Encapsulating logic into a Collection class when working with multiple items is certainly not a new idea. If you have been following along with my posts here on Building Cribbb, you will have seen me using Laravel’s Collection class and Doctrine’s ArrayCollection class frequently.

Using either of these two classes in other projects would be totally fine because both of these classes are just generic objects and have nothing that specifically ties them to their parent projects.

However, my problem with using those two classes is the fact that they are bundled in “common stuff” packages. This means if you want to only use that single class, you would end up pulling in loads of stuff you don’t need for the project.

When I’m already using Laravel or Doctrine there is little value in reinventing the wheel. However, when I’m not already using those packages I don’t see the point in including a load of files I’m never going to use just for that one class.

The Collection class

So the first thing to do is to create the Collection.php file and define the basic class:

<?php namespace PhilipBrown\Basket;

class Collection
{
    /**
     * @var array
     */
    private $items;

    /**
     * Create a new Collection
     *
     * @param array $items
     * @return void
     */
    public function __construct(array $items = [])
    {
        $this->items = $items;
    }
}

This is going to be a plain old PHP object so there is nothing to extend and no dependencies to inject.

By default we can bootstrap the class with an internal $items array. This can be overridden through the __construct() method to allow collections to be instantiated with an array of items.

Implementing the Countable interface

Next we can add a couple of native PHP interfaces that will make this class act like an array. This means we can use the object as normal, but we can also treat it like an array of items.

The first interface we will implement is Countable. By implementing this interface we can use the collection object in the native count() PHP function:

$colours = new Collection('red', 'blue', 'green']);

count($colours); // 3

If we have a look at the documentation for the Countable interface, we can see that we need to define the abstract``count() method:

<?php namespace PhilipBrown\Basket;

use Countable;

class Collection implements Countable
{
    /**
     * Count the number of items in the Collection
     *
     * @return int
     */
    public function count()
    {
        return count($this->items);
    }
}

Implementing the IteratorAggregate interface

When working with a collection, it is often useful to iterate over the collection in a foreach loop:

$colours = new Collection(["red", "green", "blue"]);

foreach ($colours as $colour) {
    // Do something
}

To enable this functionality we can implement the IteratorAggregate. Again, if we look at the documentation we can see that the interface requires that we implement a single abstract method:

IteratorAggregate extends Traversable
{
    /* Methods */
    abstract public Traversable getIterator ( void )
}

We can satisfy this interface by returning a new instance of ArrayIterator from the getIterator() method:

/**
 * Get an iterator for the items
 *
 * @return ArrayIterator
 */
public function getIterator()
{
    return new ArrayIterator($this->items);
}

The methods of a Collection

With the basic array-like functionality down we can start implementing the methods of the Collection class. This is definitely not an exhaustive list of potential methods for a Collection class, but it would make for a good starting point if you need to whip together a quick class for your project.

Firstly we need to set up a test class so we can assert that each method is working as expected as we implement them:

<?php namespace PhilipBrown\Basket\Tests;

use PhilipBrown\Basket\Collection;

class CollectionTest extends \PHPUnit_Framework_TestCase
{
    /** @var array */
    private $items = ["Homer", "Marge", "Bart", "Lisa", "Maggie"];

    /** @var Collection */
    private $collection;

    public function setUp()
    {
        $this->collection = new Collection($this->items);
    }
}

Get

The first method I will implement is get(). This will return an item from the Collection using it’s key:

/**
 * Get an item from the Collection by key
 *
 * @param mixed $key
 * @return mixed
 */
public function get($key)
{
    return $this->items[$key];
}

Because the underlying $items variable is an array, we can use the normal array syntax to return the item.

To test this method, we can write the following test:

/** @test */
public function should_get_item_by_key()
{
    $this->assertEquals('Lisa', $this->collection->get(3));
}

Add

Next we can implement the method for adding a new item to the Collection:

/**
 * Add a new item by key
 *
 * @param string $key
 * @param mixed $value
 * @return void
 */
public function add($key, $value)
{
    $this->items[$key] = $value;
}

To add an item we need to pass both a $key and a $value. Again we can use PHP’s array syntax to add the new item to the collection.

To test this method we can write the following test:

/** @test */
public function should_add_item()
{
    $this->collection->add(5, 'Snowball II');

    $this->assertEquals('Snowball II', $this->collection->get(5));
}

All

A common thing you will need to do with a collection is to return all of the items. To do this we can simply return the internal $items array:

/**
 * Get all the items of the Collection
 *
 * @return array
 */
public function all()
{
    return $this->items;
}

To test this method we can simply assert that the returned array is the same as what we defined during the set up process:

/** @test */
public function should_get_all_items()
{
    $this->assertEquals($this->items, $this->collection->all());
}

Contains

When working with a collection of items, you will usually want to determine if a specific item is in the collection. We can satisfy that requirement with the contains method:

/**
 * Check to see if a value is in the Collection
 *
 * @param mixed $value
 * @return bool
 */
public function contains($value)
{
    return in_array($value, $this->items);
}

The contains() method is simply a prettier way of using the in_array() PHP function.

To test this method we can write the following test:

/** @test */
public function should_check_for_item()
{
    $this->assertTrue($this->collection->contains('Bart'));
}

Is empty?

Similar to the contains() method, we can wrap another native PHP function to check to see if the collection is empty:

/**
 * Check to see if the Collection is empty
 *
 * @return bool
 */
public function isEmpty()
{
    return empty($this->items);
}

This method wraps the native empty() function to check the internal array.

To test this method we can write the following test:

/** @test */
public function should_check_for_emptyness()
{
    $collection = new Collection;

    $this->assertTrue($collection->isEmpty());
    $this->assertFalse($this->collection->isEmpty());
}

In this method we instantiate a new Collection and assert that it’s empty. We also assert that the Collection object that was created during the set up process is not empty.

First and Last

PHP’s array syntax can be a bit ugly, especially if you only need to get the first or last items. We can add some syntactical sugar to our Collection class by adding methods for first() and last():

/**
 * Get the first item of the Collection
 *
 * @return mixed
 */
public function first()
{
    return reset($this->items);
}

/**
 * Get the last item of the Collection
 *
 * @return mixed
 */
public function last()
{
    return end($this->items);
}

To implement these two methods we can simply wrap the native reset() and end() functions.

To assert that these two methods are working correctly we can write the following two tests:

/** @test */
public function should_get_first_item()
{
    $this->assertEquals('Homer', $this->collection->first());
}

/** @test */
public function should_get_last_item()
{
    $this->assertEquals('Maggie', $this->collection->last());
}

These two methods simply assert that the correct item of the collection was returned.

Keys

Sometimes it can be useful to get all of the array keys of a collection of items. To satisfy this requirement we can implement a keys() method:

/**
 * Return the Collection's keys
 *
 * @return array
 */
public function keys()
{
    return array_keys($this->items);
}

This method will return an array of the array keys as values. To assert that this method is working correctly we can write the following test:

/** @test */
public function should_return_the_item_keys()
{
    $keys = $this->collection->keys();

    $this->assertEquals([0,1,2,3,4], $keys);
}

Pop and Shift

If you want to get and remove the first or the last item of an array in PHP you can use the array_shift() or array_pop() functions. We can implement this on our Collection object like this:

/**
 * Get and remove the first item
 *
 * @return mixed
 */
public function shift()
{
    return array_shift($this->items);
}

/**
 * Pop the last item off the Collection
 *
 * @return mixed
 */
public function pop()
{
    return array_pop($this->items);
}

To assert that these methods are working correctly we can write the following tests:

/** @test */
public function should_get_and_remove_the_first_item()
{
    $person = $this->collection->shift();

    $this->assertEquals(4, $this->collection->count());
    $this->assertEquals('Homer', $person);
}

/** @test */
public function should_pop_the_last_item()
{
    $person = $this->collection->pop();

    $this->assertEquals(4, $this->collection->count());
    $this->assertEquals('Maggie', $person);
}

Notice how these method will also remove the item from the original array? That is sometimes an unexpected outcome, so it is something to be careful with.

Push and Prepend

If you are not working with an associative array, it can be a bit weird to specify the key you want to use. Instead of explicitly setting the key we can push an item on to the end of an array of prepend it to the start of the array:

/**
 * Push an item onto the end of the Collection
 *
 * @param mixed $value
 * @return void
 */
public function push($value)
{
    $this->items[] = $value;
}

/**
 * Push an item onto the start of the Collection
 *
 * @param mixed $value
 * @return void
 */
public function prepend($value)
{
    array_unshift($this->items, $value);
}

We can test these methods with the following two test:

/** @test */
public function should_prepend_the_collection()
{
    $this->collection->prepend('Abe');

    $this->assertEquals(6, $this->collection->count());
    $this->assertEquals('Abe', $this->collection->get(0));
}

/** @test */
public function should_push_item_onto_the_end()
{
    $this->collection->push("Santa's Little Helper");

    $this->assertEquals(6, $this->collection->count());
    $this->assertEquals("Santa's Little Helper", $this->collection->get(5));
}

In these two tests I’m simply asserting that the count of the collection increased and the item has been inserted in the correct position.

Remove

The normal way of removing an item from an array is by using the unset() method. We can make this prettier by wrapping it in a remove() method:

/**
 * Remove an item from the Collection by key
 *
 * @param mixed $key
 * @return void
 */
public function remove($key)
{
    unset($this->items[$key]);
}

We can assert this method is working correctly with the following test:

/** @test */
public function should_remove_item()
{
    $this->collection->remove(0);

    $this->assertEquals(4, $this->collection->count());
}

In this test I’m simply asserting that the collection’s count is reduced by one.

Sometimes we want to search for a value and return the correct key from the array. This can be very useful when you need to know the key of a value but you have no way of knowing what it is.

To implement this functionality we can use PHP’s native array_search() function:

/**
 * Search the Collection for a value
 *
 * @param mixed $value
 * @return mixed
 */
public function search($value)
{
    return array_search($value, $this->items, true);
}

Note the true third argument of the function. I’ve set this so the search will only look for values of the correct type or instance. If you wanted to allow this argument to be overridden you could set a default in the method argument list that would be passed to the function.

To test this method we can write the following test:

/** @test */
public function should_search_the_collection()
{
    $key = $this->collection->search('Bart');

    $this->assertEquals(2, $key);
}

In this test I’m searching for a value and then asserting that the correct key is returned.

Values

When manipulating a collection of items, the keys of the array can sometimes get out of whack. This can cause unforeseen bugs if not dealt with because you expect the keys of an indexed array to have certain default attributes.

In order to fix this problem we can use PHP’s array_values() function to reset the keys of the collection:

/**
 * Reset the values of the Collection
 *
 * @return void
 */
public function values()
{
    $this->items = array_values($this->items);
}

The array_values() function will return the values of the $this->items array as a new array (and therefore the keys will be reset).

Sort

When you have a collection of items it will often be required that you sort them in a particular way to be displayed correctly in a view. We can offer the ability to sort the collection by wrapping PHP’s uasort() function:

/**
 * Sort through each item with a callback
 *
 * @param Closure $callback
 * @return void
 */
public function sort(Closure $callback)
{
    uasort($this->items, $callback);
}

Notice how the second parameter is a Closure? A Closure is an anonymous function (What are PHP Lambdas and Closures?). This means we can pass a function into the method to define how we want the collection sorted.

An example of this in action can be seen as we write the test:

/** @test */
public function should_sort_items_in_collection_and_reset_keys()
{
    $this->collection->sort(function ($a, $b) {
        if ($a == $b) return 0;

        return ($a < $b) ? -1 : 1;
    });

    $this->collection->values();

    $this->assertEquals('Bart', $this->collection->get(0));
    $this->assertEquals('Homer', $this->collection->get(1));
    $this->assertEquals('Lisa', $this->collection->get(2));
    $this->assertEquals('Maggie', $this->collection->get(3));
    $this->assertEquals('Marge', $this->collection->get(4));
}

In this test I’m passing in a function that will sort the values into alphabetical order. However notice how this will not automatically reset the keys? To do that I’ve used the values() method from earlier.

Each

A common thing you will want to do to a collection is iterate over each item in the collection.

We’ve already seen how we can add native array characteristics to the collection so we can use foreach constructs. But sometimes it’s preferable to iterate use an object oriented approach.

We can implement this functionality by wrapping PHP’s array_map() function with an each() method:

/**
 * Run a callback on each item
 *
 * @param Closure $callback
 * @return void
 */
public function each(Closure $callback)
{
    array_map($callback, $this->items);
}

As with the sort() method, this method also accepts a Closure so you can pass in a function to manipulate the items in any way you want.

An example of this can be seen in the test:

/** @test */
public function should_run_callback_on_each_item()
{
    $this->collection->each(function ($person) {
        $this->assertTrue(is_string($person));
    });
}

In this test I’m iterating over each item and asserting that the value is a string.

Filter

If you have an collection of items, but you only wanted to keep certain items based on some requirement, you would want to apply a filter to remove the items that didn’t match.

We can implement this functionality by using PHP’s very useful array_filter() function:

/**
 * Filter the Collection and return a new Collection
 *
 * @param Closure $callback
 * @return Collection
 */
public function filter(Closure $callback)
{
    return new Collection(array_filter($this->items, $callback));
}

Once again this method accepts a Closure so we can specify the filter by passing in a function. Notice also how I’m creating a new Collection instance from the return values of the functional call.

An example of this method in action can be seen in the test:

/** @test */
public function should_filter_the_collection()
{
    $filtered = $this->collection->filter(function ($person) {
        return substr($person, 0,1) === 'M';
    });

    $this->assertEquals(2, $filtered->count());
}

In this test I’m filtering the collection of items to only return the values where the first letter is M. In this case I have two values that match that criteria and so I can assert that the count of the returned collection should be 2.

Map

And finally we have my favourite method on the Collection object. When you want to iterate over a collection of items and manipulate them in some form, PHP’s array_map() function is exactly what you need:

/**
 * Run a Closure over each item and return a new Collection
 *
 * @param Closure $callback
 * @return Collection
 */
public function map(Closure $callback)
{
    return new Collection(array_map($callback, $this->items, array_keys($this->items)));
}

Once again because we are manipulating the collection we need to return a new instance of Collection with the new values.

To test this method we can write the following test:

/** @test */
public function should_map_each_item()
{
    $family = $this->collection->map(function ($person) {
        return "$person Simpson";
    });

    $this->assertEquals('Homer Simpson', $family->get(0));
    $this->assertEquals('Marge Simpson', $family->get(1));
    $this->assertEquals('Bart Simpson', $family->get(2));
    $this->assertEquals('Lisa Simpson', $family->get(3));
    $this->assertEquals('Maggie Simpson', $family->get(4));
}

In this test I’m iterative over each item and appending the person’s second name. We can assert that the method worked correctly by checking the values of the returned Collection instance.

Conclusion

In my opinion, I think it is much easier to work with a collection of items as an object, rather than an array. Arrays and PHP’s native array functions are very useful, but you are on a slippery slope to leaking responsibility.

An object will often require an internal collection of items to work with. But it is not the job of the parent object to be responsible for implementing logic to manipulate the collection.

By writing a collection class we can encapsulate the logic of how items can be manipulated.

We can then use this plain PHP class internally to other objects to provide that functionality and prevent code duplication. With the Collection class being just a plain PHP object, we can just instantiate internally to the parent class and forget that its there.

This not only move the responsibility to the correct place, it also dramatically cleans up your code.

So the next time you need to work with a collection of items, don’t let that logic leak out! Instead encapsulate the logic in a Collection class.

Hopefully today’s article will have introduced you to the wonderful world of Collection objects and perhaps some of the less well known native PHP array functions.

Next week we will be looking at the Product object. For a sneak peak at what’s to come, take a look at the git repository on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.