www.voj-tech.net


Collections in vtCompose

VTCompose\Collection is an important namespace in vtCompose. It is referenced in many other namespaces making it a core building block of the framework. In this article we look at four different types of collections available in vtCompose. On examples we show how collections are populated with elements as well as how elements are retrieved from them. Following that, we look at a brief introduction to general IEnumerable interface operations implemented by all types of collections. These operations are similar to those provided by the LINQ component in .NET Framework.

Initial Setup

A bit of a boring boilerplate code must come first—let us include the vtCompose class loader, import any classes and interfaces which will be referenced later and register the class loader and also the vtCompose error handler. Should a PHP error occur the handler will throw a VTCompose\ErrorHandling\ErrorException while respecting the error_reporting php.ini directive (more on error handling on the Error Handling page).

We can then define a simple class to represent our collection elements. This will be a class called Person. A person shall have a name and an age.

<?php

require_once 'VTCompose/Autoloading/Autoloader.php';

use VTCompose\Autoloading\Autoloader;
use VTCompose\Collection\ArrayList;
use VTCompose\Collection\Collection;
use VTCompose\Collection\Dictionary;
use VTCompose\Collection\ICollection;
use VTCompose\Collection\IDictionary;
use VTCompose\Collection\IList;
use VTCompose\Collection\ISet;
use VTCompose\Collection\Set;
use VTCompose\ErrorHandling\ErrorHandler;

(new Autoloader())->register();
(new ErrorHandler())->register();

class Person {

    private $name;
    private $age;

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }

    public function setName($name) { $this->name = $name; }
    public function getName() { return $this->name; }

    public function setAge($age) { $this->age = $age; }
    public function getAge() { return $this->age; }

}

The Collection Class

The simplest collection type in vtCompose is the Collection class which implements the ICollection interface. In this type of collection there is a bunch of elements, possibly of mixed types, and although they are physically stored in a specific order the semantics of the collection does not define any order.

Let us start by defining a function to dump the contents of an ICollection instance to the output, assuming it contains instances of the Person class, and to print the number of elements in the collection. We also create two instances of the Person class to use in our experiments.

function dumpCollectionOfPeople(ICollection $people) {
    foreach ($people as $person) {
        echo "{$person->getName()} is {$person->getAge()} years old.\n";
    }

    echo "Total number of people: {$people->count()}\n";
}

$eliza = new Person('Eliza', 37);
$rudolph = new Person('Rudolph', 25);

The following example shows possibly the most intuitive way of populating a collection. To add a value to our collection we simply call the ICollection::add() method passing our value as an argument. Note that since instances of the Collection class can contain duplicates the resulting number of elements in the collection is 3.

$people = new Collection();
$people->add($eliza);
$people->add($eliza);
$people->add($rudolph);
dumpCollectionOfPeople($people);

Output:

Eliza is 37 years old.
Eliza is 37 years old.
Rudolph is 25 years old.
Total number of people: 3

Another way to initialise a collection is to pass a PHP array with the values to the Collection constructor.

$people = new Collection([$eliza, $eliza, $rudolph]);
dumpCollectionOfPeople($people);

Output:

Eliza is 37 years old.
Eliza is 37 years old.
Rudolph is 25 years old.
Total number of people: 3

Finally, we can also initialise a collection by making a copy of an existing one.

$copyOfPeople = new Collection($people);
dumpCollectionOfPeople($copyOfPeople);

Output:

Eliza is 37 years old.
Eliza is 37 years old.
Rudolph is 25 years old.
Total number of people: 3

The ICollection::contains() method allows us to check whether a value is present in the collection. Although we are checking for a 25-year-old Rudolph we will not get a positive response unless we pass the original instance.

$rudolph2 = new Person('Rudolph', 25);
echo "The first Rudolph is " . ($people->contains($rudolph) ? "" : "not ") . "among the people but ";
echo "the second Rudolph is " . ($people->contains($rudolph2) ? "" : "not ") . "among the people.\n";

Output:

The first Rudolph is among the people but the second Rudolph is not among the people.

To remove a value from a collection simply call the ICollection::remove() method passing the value to remove as an argument.

$people->remove($eliza);
dumpCollectionOfPeople($people);

Output:

Eliza is 37 years old.
Rudolph is 25 years old.
Total number of people: 2

To clear a collection call the ICollection::clear() method.

$people->clear();
dumpCollectionOfPeople($people);

Output:

Total number of people: 0

The Set Class

The Set class implements the ISet interface which extends the ICollection interface without adding any new methods. A set in vtCompose is the same as an instance of the Collection class except that it cannot contain duplicates.

$people = new Set([$eliza, $eliza, $rudolph]);
dumpCollectionOfPeople($people);

Output:

Eliza is 37 years old.
Rudolph is 25 years old.
Total number of people: 2

If we try to add a value already present in the set nothing will happen, the operation will be silently ignored.

$people->add($eliza);
dumpCollectionOfPeople($people);

Output:

Eliza is 37 years old.
Rudolph is 25 years old.
Total number of people: 2

The ArrayList Class

In case of the ArrayList class, which implements the IList interface, every element has a defined zero-based index within the collection. Let us define a function to dump the contents of an IList instance to the output, again, assuming it only contains instances of the Person class, including the index for every element. We also create another instance of Person to have more objects to play with.

function dumpListOfPeople(IList $people) {
    foreach ($people as $index => $person) {
        echo "Person at index {$index} is {$person->getName()}.\n";
    }
}

$matt = new Person('Matt', 39);

Let us start by initialising a list of 3 people.

$people = new ArrayList([$eliza, $rudolph, $matt]);
dumpListOfPeople($people);

Output:

Person at index 0 is Eliza.
Person at index 1 is Rudolph.
Person at index 2 is Matt.

The IList interface allows us to remove an element from a list by specifying the element index.

$people->removeAt(1);
dumpListOfPeople($people);

Output:

Person at index 0 is Eliza.
Person at index 1 is Matt.

Similarly we can insert a value to the list specifying the index (position) for the insertion. All the elements following the position move down by one index to accommodate the new element.

$people->insert(1, $rudolph);
dumpListOfPeople($people);

Output:

Person at index 0 is Eliza.
Person at index 1 is Rudolph.
Person at index 2 is Matt.

The IList::indexOf() method returns the index of a searched value. It returns -1 in case the value is not present in the list.

$xenia = new Person('Xenia', 20);
echo "{$matt->getName()} is at index {$people->indexOf($matt)} but ";
echo "{$xenia->getName()} is at index {$people->indexOf($xenia)}.\n";

Output:

Matt is at index 2 but Xenia is at index -1.

Because the IList interface extends the PHP ArrayAccess interface and the ArrayList class ultimately implements it we can access list elements using the square bracket notation.

$eliza = $people[0];
$people[2] = $eliza;
dumpListOfPeople($people);

Output:

Person at index 0 is Eliza.
Person at index 1 is Rudolph.
Person at index 2 is Eliza.

It is also possible to append a value to the end of a list by using the syntax with an empty pair of square brackets.

$people[] = $xenia;
dumpListOfPeople($people);

Output:

Person at index 0 is Eliza.
Person at index 1 is Rudolph.
Person at index 2 is Eliza.
Person at index 3 is Xenia.

The Dictionary Class

In PHP we have associative arrays but we cannot use objects as keys unless we create some sort of hash ids or other unique identifiers of the objects and use them as keys instead. This is exactly what the Dictionary class encapsulates. Because the square bracket notation and the PHP ArrayAccess interface allow using objects within the square brackets we are free to implement a dictionary in PHP with a transparent translation of objects into hash ids in the background.

The Dictionary class implements the IDictionary interface which extends the ICollection and ArrayAccess interfaces. In the following code snippet we define a function taking an IDictionary instance to tell us what the favourite ice cream of each person in a dictionary is. Furthermore we define a function to simply dump a collection of strings (ice creams in our case) to the output including the number of the strings in the collection.

function dumpFavouriteIceCreamDictionary(IDictionary $favouriteIceCreamDictionary) {
    foreach ($favouriteIceCreamDictionary as $person => $favouriteIceCream) {
        echo "{$person->getName()}'s favourite ice cream is $favouriteIceCream.\n";
    }
}

function dumpCollectionOfIceCreams(ICollection $iceCreams) {
    foreach ($iceCreams as $iceCream) {
        echo "$iceCream\n";
    }

    echo "Total number of ice creams: {$iceCreams->count()}\n";
}

One way to map a key to a value is to use the IDictionary::addAt() method which takes the key and the value as parameters.

$favouriteIceCreamDictionary = new Dictionary();
$favouriteIceCreamDictionary->addAt($eliza, 'chocolate');
$favouriteIceCreamDictionary->addAt($matt, 'strawberry');
$favouriteIceCreamDictionary->addAt($rudolph, 'vanilla');
dumpFavouriteIceCreamDictionary($favouriteIceCreamDictionary);

Output:

Eliza's favourite ice cream is chocolate.
Matt's favourite ice cream is strawberry.
Rudolph's favourite ice cream is vanilla.

The IDictionary::getKeys() and IDictionary::getValues() methods allow us to retrieve an instance of the ICollection interface containing the keys of the dictionary and the values of the dictionary, respectively.

dumpCollectionOfPeople($favouriteIceCreamDictionary->getKeys());
dumpCollectionOfIceCreams($favouriteIceCreamDictionary->getValues());

Output:

Eliza is 37 years old.
Matt is 39 years old.
Rudolph is 25 years old.
Total number of people: 3
chocolate
strawberry
vanilla
Total number of ice creams: 3

To check whether a key is present in a dictionary we can either use the IDictionary::containsKey() method or the PHP isset() language construct in combination with the square bracket notation.

$hasFavouriteIceCream = $favouriteIceCreamDictionary->containsKey($rudolph);
echo "The first Rudolph does " . ($hasFavouriteIceCream ? "" : "not ") . "have ";
echo ($hasFavouriteIceCream ? "a " : "any ") . "favourite ice cream\n";

$hasFavouriteIceCream = isset($favouriteIceCreamDictionary[$rudolph2]);
echo "however the second Rudolph does " . ($hasFavouriteIceCream ? "" : "not ") . "have ";
echo ($hasFavouriteIceCream ? "a " : "any ") . "favourite ice cream.\n";

Output:

The first Rudolph does have a favourite ice cream
however the second Rudolph does not have any favourite ice cream.

Removing a key and the associated value can be done by calling the IDictionary::removeAt() method which takes the key as a parameter.

$favouriteIceCreamDictionary->removeAt($eliza);
dumpFavouriteIceCreamDictionary($favouriteIceCreamDictionary);

Output:

Matt's favourite ice cream is strawberry.
Rudolph's favourite ice cream is vanilla.

The Dictionary class implements the ICollection interface in such a way that the ICollection elements are KeyValuePair objects. To access these objects we can use the IEnumerable::toArray() method. The method returns a PHP array values of which are exactly what we are after.

foreach ($favouriteIceCreamDictionary->toArray() as $keyValuePair) {
    echo "{$keyValuePair->getKey()->getName()} fancies {$keyValuePair->getValue()} ice cream.\n";
}

Output:

Matt fancies strawberry ice cream.
Rudolph fancies vanilla ice cream.

We should mention that it is possible to read and write values of a dictionary based on a key using the square bracket notation as in the following example.

$iceCream = $favouriteIceCreamDictionary[$rudolph];
$favouriteIceCreamDictionary[$eliza] = $iceCream;
dumpFavouriteIceCreamDictionary($favouriteIceCreamDictionary);

Output:

Matt's favourite ice cream is strawberry.
Rudolph's favourite ice cream is vanilla.
Eliza's favourite ice cream is vanilla.

Apart from calling the previously mentioned IDictionary::removeAt() method another way of removing a key and the associated value from a dictionary is by using the PHP unset() language construct in combination with the square bracket notation.

unset($favouriteIceCreamDictionary[$eliza]);
dumpFavouriteIceCreamDictionary($favouriteIceCreamDictionary);

Output:

Matt's favourite ice cream is strawberry.
Rudolph's favourite ice cream is vanilla.

Enough of ice cream examples. Let us have a look at general methods from the IEnumerable interface which is implemented by all of the collection types.

The IEnumerable Interface

Any vtCompose collection implements the IEnumerable interface, which extends the PHP IteratorAggregate and Countable interfaces, and as such allows iterating over its elements using the foreach construct. There are however additional operations included in the IEnumerable interface. These additional operations will remind the savvy reader of LINQ in .NET Framework. In CLI languages such as C# or Visual Basic .NET LINQ is implemented using extension methods. In PHP we cannot write extension methods and so the operations are part of the actual interface. Also we are not getting the elegant and shiny syntax of LINQ but at least we are getting the functionality.

Already in the first example we use three IEnumerable methods; we order our list of people by their age in descending order, in case there are people with the same age we order them by their names and finally we create a new list based on the result. We use anonymous functions to define our order key selectors.

$people = new ArrayList([
    new Person('Inspector Clouseau', 60),
    new Person('Cookie Monster', 25),
    new Person('Frank Drebin', 52),
    new Person('Donnie Darko', 24),
    new Person('Captain Kirk', 54),
    new Person('Tony Montana', 40),
    new Person('Rocky Balboa', 28),
    new Person('Forrest Gump', 28),
    new Person('Darth Vader', 40),
    new Person('Indiana Jones', 38),
]);

$peopleOrderedByAgeAndName = $people
    ->orderByDescending(function(Person $p) { return $p->getAge(); })
    ->thenBy(function(Person $p) { return $p->getName(); })
    ->toArrayList();

dumpCollectionOfPeople($peopleOrderedByAgeAndName);

Output:

Inspector Clouseau is 60 years old.
Captain Kirk is 54 years old.
Frank Drebin is 52 years old.
Darth Vader is 40 years old.
Tony Montana is 40 years old.
Indiana Jones is 38 years old.
Forrest Gump is 28 years old.
Rocky Balboa is 28 years old.
Cookie Monster is 25 years old.
Donnie Darko is 24 years old.
Total number of people: 10

In the following example we use the methods IEnumerable::first() and IEnumerable::last() to retrieve the first and last elements from the previously created list.

echo "The youngest person is {$peopleOrderedByAgeAndName->last()->getName()} and ";
echo "the oldest person is {$peopleOrderedByAgeAndName->first()->getName()}.\n";

Output:

The youngest person is Donnie Darko and the oldest person is Inspector Clouseau.

Just like in LINQ the IEnumerable::single() method will verify that the sequence yields a single element only. If that is the case it will return the object otherwise it will throw an InvalidOperationException.

$singleElementEnumerable = new Collection(['single element']);
echo "{$singleElementEnumerable->single()}\n";

Output:

single element

There is an alternative to each of the last three mentioned methods which returns NULL should the sequence yield no elements at all.

$emptyEnumerable = new Collection();
var_dump($emptyEnumerable->firstOrNull());
var_dump($emptyEnumerable->lastOrNull());
var_dump($emptyEnumerable->singleOrNull());

Output:

NULL
NULL
NULL

The following example shows how to use the IEnumerable::toDictionary() method to construct a dictionary from any sequence by specifying a key selector. (The method has an optional second parameter which allows us to specify a value selector.)

$hashFunction = new VTCompose\Data\HashFunction\Fnv1a();
$dictionaryOfPeople = $people->toDictionary(function(Person $p) use ($hashFunction) {
    return $hashFunction->computeHash($p->getName());
});

foreach ($dictionaryOfPeople as $hash => $person) {
    echo "{$person->getName()}'s hash is $hash.\n";
}

Output:

Inspector Clouseau's hash is 533a711c1bf58be7.
Cookie Monster's hash is e575892404a86ad9.
Frank Drebin's hash is 30bff8b812442fab.
Donnie Darko's hash is c4a647bdd26f5d57.
Captain Kirk's hash is e06ab69a3a86e41a.
Tony Montana's hash is a642b0d1d3528411.
Rocky Balboa's hash is 2ca07ec29eed78ce.
Forrest Gump's hash is 068586ed9eef1f63.
Darth Vader's hash is 220c87f3b33ec31c.
Indiana Jones's hash is ec1fbc9b340966ea.

The IEnumerable interface currently does not declare all the operations one might expect to be included in it. A workaround for this is to convert the sequence to a PHP array, use the appropriate PHP array function and convert the result back to a vtCompose collection.

$reversedPeople = new ArrayList(array_reverse($people->toArray()));
dumpListOfPeople($reversedPeople);

Output:

Person at index 0 is Indiana Jones.
Person at index 1 is Darth Vader.
Person at index 2 is Forrest Gump.
Person at index 3 is Rocky Balboa.
Person at index 4 is Tony Montana.
Person at index 5 is Captain Kirk.
Person at index 6 is Donnie Darko.
Person at index 7 is Frank Drebin.
Person at index 8 is Cookie Monster.
Person at index 9 is Inspector Clouseau.

An example of an important method missing in the IEnumerable interface would be the IEnumerable::where() method which would take a predicate and filter a sequence of values based on it. In fact many of the methods mentioned above should be able take an optional predicate to filter a sequence as well. Such methods are strong candidates for the next additions to the VTCompose\Collection namespace functionality.