Managur Collections is a library that provides a fully featured collection object to PHP.
Collections are similar to an array in PHP, but can be restricted to a specific type, both at the index and value level.
Collections also provide some built in functionality, such as being able to easily search the contents for a particular value, perform filtering, and to iterate through within a closed scope.
Let's take a look at some examples:
Before we can use Managur Collections, we need to install it. The recommended method is using PHP's package manager, Composer:
$ composer require managur/collection
We'll assume that you have installed via Composer, and therefore are using Composer's autoloader.
There are two ways in that you can use Managur Collections. The first is using its class constructor, as follows:
<?php
use Managur\Collection\Collection;
$collection = new Collection(['your', 'collectible', 'data']);
The second way is to use the handy collect()
function which is included:
$collection = collect(['your', 'collectible', 'data']);
And now you have a collection!
Collections are great for type guarantees. If you're passing an array around, there are no guarantees that the array indexes or values are of any given type, which means that you will regularly need to check them around your application.
Managur Collections are easily constrained to a type, either by extending the
base Collection
class yourself, or by using the provided factory methods.
If you need a collection that can ONLY have integer values, you can extend Managur Collections with your own class as follows:
<?php declare(strict_types=1);
use Managur\Collection\Collection;
final class IntegerCollection extends Collection
{
protected $valueType = 'integer';
}
Now, when you construct or append data to your IntegerCollection
, if the data
that you're trying to add to the collection isn't of the type Integer
, a
TypeError
exception will be thrown which you can catch immediately.
Fail fast.
Similar to enforcing a type for the values, you can also fix the index (or key) types:
<?php declare(strict_types=1);
use Managur\Collection\Collection;
final class IntegerKeyCollection extends Collection
{
protected $keyType = 'integer';
}
It is now impossible to use an index that is not an integer for this specific collection type.
You can combine the key and value restrictions to force a very specifically typed collection if you require.
When you append()
or offsetSet()
a value to the collection, we call the
protected keyStrategy()
method, passing it the item that you want to collect.
By default we return null
, which tells append()
and offsetSet()
that you
wish for the collection to use PHP’s default zero-indexed, auto-incrementing
key IDs.
If this is not the case, whether because you simply wish to organise by your
own keys, or because you’ve restricted the key type to a string
, for example,
you can override the keyStrategy()
method in your child collection class to
automatically set the key to a value of your choosing.
For example, in our hypothetical UserCollection
class, we add this
implementation of keyStrategy()
:
protected function keyStrategy($value)
{
return $value->id();
}
And once the key strategy is set, appending or setting items in the collection will have the key set appropriately.
$user = new User('my-user-id', 'Managur User');
$collection = new UserCollection();
$collection[] = $user;
var_dump($collection['my-user-id'] === $user); // returns true
We provide three methods that will generate an anonymous class with the same functionality as the main collection, but with enforced types for keys and/or values:
use Managur\Collection\Collection;
$integerValueCollection = Collection::newTypedValueCollection('integer', [1,2,3,4,5]);
$integerKeyCollection = Collection::newTypedKeyCollection('integer', ['a','b',1]);
$integerCollection = Collection::newTypedCollection('integer', 'integer', [1,2,3,4,5]);
If you wish, you can use the newTypedCollection()
method for all purposes,
passing in null
for the key (first argument) and/or the value (second
argument) to get the specific combination that you require.
This library uses PHPUnit 7 to provide unit tests. To run the tests yourself, simply enter the following at a command line interface:
$ composer test
Tests should run automatically, assuming that you can call PHP directly from the command line (ie it is in your path).
We also automate testing with Github Actions. You can view the latest builds here.
We enforce PSR-2 coding standards. To test the code, run this command:
$ composer cs
To automatically fix code standard failures, run this command:
$ composer cs-fix
Full functionality provided with Managur Collection is documented here:
Important: Appending will fail if you are using typed indexes
Data can be appended to a collection in one of two ways:
$collection[] = 'new data';
$collection->append('new data');
If you need to set a value to a specific index within the collection, do so as follows:
Note: If there was already data at this index, it will be overwritten
$collection[4] = 'new data';
$collection->offsetSet(4, 'new data');
map()
provides a method to iterate over the collection, modifying them and
returning a new collection with the changes. It will accept any callable
as a
parameter.
Because this function operates within a private scope, it protects any
variables that exist outside of the loop, as well as preventing data within
its scope from leaking into your wider code. For this reason, map()
is
recommended over a simple foreach()
loop in most cases.
Here's a simple example of doubling the values of each value:
$collection = new Collection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
$doubles = $collection->map(function ($value) {
return $value * 2;
});
The mapInto()
method allows you to take any collection and in one call create
a new collection of a given type, run a callable across the data, and have it
immediately collected into the new collection.
Here's an example where we have created new collections that are restricted to allowing only integers to be collected in the first, and strings in the second:
$ints = new IntegerCollection([1, 2, 3, 4, 5]);
$strings = $ints->mapInto(function (int $item): string {
return (string)($item*10);
}, StringCollection::class);
We now have a StringCollection
which contains a string representation of
every value from the original $ints
collection, multiplied by 10.
slice()
provides a method to return the sequence of elements from the array
This works on the position in the array, not the key.
Here's a simple example slicing the first 2 elements from the array:
$collection = new Collection(['a' => 1, 'b' => 2, 'c' => 3]);
$doubles = $collection->slice(0, 2); // ['a' => 1, 'b' => 2]
The PHP documentation
has a full description of the way $offset
and $limit
behave.
Sometimes we need to iterate over values, but we don't want to make any
changes. That is where each()
is different to map()
. Otherwise, the
functionality is identical.
Referring back to the map()
example, here is an implementation of each()
where we simply echo
the doubled value, instead:
$collection = new Collection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
$collection->each(function ($value) {
echo $value * 2;
});
We often loop over data to concatenate results. Maybe we want to get a list of
all email addresses from a collection of users. This is where reduce()
is our
go-to method. You can reduce into anything.
The reduce()
method requires a callback which takes two arguments; the
carry variable that we'll be appending to in each iteration, and the value
that we'll be using.
reduce()
takes a final argument, which is the initialising carry value.
This is optional, in which case null
will be passed into your callback in the
first iteration, so ensure that your callback will accept a null value. We do
not recommend this, however, and suggest that you always provide an initial
value of the same type that you want to be returned when the reduction is
completed.
In this example, we'll pass in an empty array as our initial value, as you can see on the last line:
$collection = new Collection($arrayOfUserObjects);
$emails = $collection->reduce(function ($carry, $user) {
$carry[] = $user->email();
return $carry
}, []);
It's common for people to forget to apply the value to $carry
and return
$carry
at the end. Don't forget this, else you'll find that everything
reduces to null
.
If you need to remove values from your collection, you can do so using the
filter()
method. If you call it without any arguments then any values that
match the same criteria that the core empty()
function returns true
for,
will be removed from the results.
You can optionally provide a callback that should return true
if the data
should be retained, or false
to be filtered out.
Note: $filter()
returns a new copy of the collection without the filtered
values. It does not filter the original collection.
Here's an example where we will filter out all inactive users from our user collection:
$collection = new Collection($arrayOfUserObjects);
$filtered = $collection->filter(function ($user) {
return $user->isActive();
});
Nice and simple, and again we're protecting scope so that we're not accidentally overwriting anything in our own code.
The first()
method optionally takes a callback as an argument. If you do not
provide this then it will return the first non-empty value from the collection.
If you do provide a callback, it will return the first value that matches the callback's criteria by returning true.
Let's look at an example where we get the first user who has a Gmail email address:
$collection = new Collection($arrayOfUserObjects);
$firstGmailUser = $collection->first(function ($user) {
return false !== stripos($user->emailAddress(), '@gmail.');
});
last()
is exactly the same as first()
, with the (hopefully obvious)
exception that it returns the last entry:
$collection = new Collection($arrayOfUserObjects);
$lastGmailUser = $collection->last(function ($user) {
return false !== stripos($user->emailAddress(), '@gmail.');
});
If you need to check whether a specific value exists within your collection, this is the method for you. You can also search with a callback, providing you with a really flexible search operation where you can match on partial or computed data.
contains()
returns a boolean value, so you can use this to check conditions.
$collection = new Collection($arrayOfUserObjects);
$containsAGmailUser = $collection->contains(function ($user) {
return false !== stripos($user->emailAddress(), '@gmail.');
});
If a user with a Gmail address is found in the collection then
$containsAGmailUser
will be true
.
The methods isEmpty()
and isNotEmpty()
are available to check whether the
collection contains items or not.
$collection = new Collection([]);
$collection->isEmpty(); // true
$collection = new Collection([1, 2, 3]);
$collection->isNotEmpty(); // true
The push()
method is similar to append()
, in that it will only work if you
have not defined a strict index type. The only way that it is different is that
you can push multiple values in one operation:
$collection->push(5, 6, 7, 8, 9);
pop()
is a typical pop method, where the latest value will be returned, while
simultaneously removed from the collection:
$collection = new Collection([1, 2, 3, 4, 5]);
$popped = $collection->pop();
Now, $popped
will equal 5
, and the collection itself will now only contain
the numbers 1 through 4.
If you have two compatible collections (ie they're not restricted to different types), you can merge them in one operation:
$collection1 = new Collection([1,2,3,4,5]);
$collection2 = new Collection([6,7,8,9,10]);
$merged = $collection1->merge($collection2);
Note that a brand new collection is returned, so both $collection1
and
$collection2
will remain as they were before.
The into()
method provides a readable method to copy the collection into
another compatible collection:
$collection1 = new Collection([1,2,3,4,5]);
$collection2 = $collection1->into(IntegerCollection::class);
There is also a provided function that collects directly into your preferred collection class:
$collection = collectInto(IntegerCollection::class, [1,2,3,4,5]);
If you need to sort your collection data, this is the method for you.
sort()
will accept a callable if you wish to use a user-defined sorting
algorithm, or you can call it with no arguments.
This is analogous to using the sort()
and usort()
functions with a simple
array.
If you have specified a fixed index type then we will maintain the indexes for
each value, similar to calling asort()
or uasort()
on a normal array.
$collection = new Collection([5,2,6,4,7,1]);
$sorted = $collection->sort();
$alsoSorted = $collection->sort(function ($a, $b) {
return $a <=> $b;
});
This method will return a copy of the collection with the sorting applied.
Similar to sort()
, the asort()
method optionally accepts a callback to sort
by a custom algorithm. The only difference between sort()
and asort()
is
that index associations are maintained, regardless of whether the collection
key type is constrained or not.
$collection = new Collection([5,2,6,4,7,1]);
$sorted = $collection->asort();
$alsoSorted = $collection->asort(function ($a, $b) {
return $a <=> $b;
});
This method will return a copy of the collection with the sorting applied.
The shuffle()
method will take your values, shuffle them randomly, and then
return a new instance of the collection with the shuffled data:
$collection = new Collection([1,2,3,4,5]);
$shuffled = $collection->shuffle();
shuffle()
also takes an optional integer seed, which will be used to
determine the start state of the pseudo random number generator (mt_rand()
)
that is used to determine the order that elements are shuffled into.
If you use the seed with shuffle()
, the resulting collection will always
have its elements in the same order for a given original collection and seed
value. It will never return a different order for this combination.
The implode()
method will take your collection and return a string with the
items joined together with $glue
$collection = new Collection(['a', 'b']);
$joined = $collection->implode(', '); // 'a, b'
You may also pass an optional callable as the second argument which allows you to pick the specific properties from your collected objects when imploding:
$collection = new Collection($users);
$joined = $collection->implode(', ', function (User $user): string {
return $user->name();
});