Tale Collection is a heavily iterator-based Collection implementation for PHP.
It comes with a generic Collection class that handles chained iteration well as well as a Map and Set implementation with a solid API.
composer require talesoft/tale-collection
Tale\collection(iterable $iterable): Tale\CollectionInterface
//or
new Tale\Collection(iterable $iterable)
use function Tale\collection;
$collection = collection(['a', 'b', 'c', 'd']))
->filter(function (string $char) {
return $char !== 'b';
})
->map(function (string $char) {
return strtoupper($char)
})
->flip();
var_dump($collection['c']); //int(2)
var_dump($collection->toArray());
/*
array(3) {
'a' => int(0)
'c' => int(2)
'd' => int(3)
}
*/
As stated, it's heavily iterator-based, so many stacked operations won't ever turn anything into an actual array unless you actually access the data.
Let's demonstrate!
use function Tale\collection;
funuction generateAtoZ()
{
//Imagine this is some hard work
yield from range('a', 'z');
}
$collection = collection(generateAtoZ())
//We filter all a-s
->filter(function (string $char) {
return $char !== 'a';
})
//We also chain a normal SPL RegexIterator and drop all i to z-s
->chain(\RegexIterator::class, '/[^i-z]/')
//Uppercase all characters
->map(function (string $char) {
return strtoupper($char);
})
//Flip the keys and values
->flip()
//Drop characters with the index 3 (this will be 'C')
->filter(function (int $index) {
return $index !== 2;
})
//Flip again
->flip()
//Only yield the values (array_values, basically)
->getValues();
//This right below is the _first_ part where generateAtoZ will
//actually be triggered at all! Until then we basically did nothing
//except for chaining some iterators
var_dump($collection[2]); //string(1) "E"
//Notice how it's E, because we dropped A and C
var_dump($collection->toArray());
/*
array(6) {
[0] => string(1) "B"
[1] => string(1) "D"
[2] => string(1) "E"
[3] => string(1) "F"
[4] => string(1) "G"
[5] => string(1) "H"
}
*/
Most methods of Tale collections return collections again. Collections don't only contain arrays, but can also contain an iterable. The iterable will be passed through most methods you work with and will only be lazy-triggered when you access keys or cast it to an array.
This gives the possibility to deeply modify iterable values without creating array copies all over.
In fact, not a single of the methods called in the example above except for toAray()
ever
triggered the generateAtoZ()
generator, not even getValues()
or flip()
. They all return collections
containing a nested specific iterator.
This also leads to some nice side-effects, like flip()
not overwriting
values.
Imagine we want to do something with the keys of an array without overwriting its values when flipping (very memory efficiently):
use function Tale\collection;
$values = collection(['a' => 2, 'b' => 2, 'c' => 3])
->flip()
->map(function (string $key) {
return strtoupper($key);
})
->flip();
var_dump($values->toArray());
/*
array(3) {
'A' => int(2)
'B' => int(2)
'C' => int(3)
}
*/
To understand what happened, let's look at the native PHP equivalent
use function Tale\collection;
$values = array_flip(['a' => 2, 'b' => 2, 'c' => 3]);
//Here our values are lost already, it will result in
// [2 => 'b', 3 => 'c'], as keys are unique
$values = array_map(function (string $key) {
return strtoupper($key);
}, $values);
$values = array_flip(['a' => 2, 'b' => 2, 'c' => 3]);
var_dump($values);
/*
array(2) {
["B"]=> int(2)
["C"]=> int(3)
}
*/
Through the way iterators work, the values are simply never reduced to an actual array, only when retrieving the end-result. Duplicate keys are no problem during iteration and will always stay available until the iterable gets casted to an array internally.
All of the following collections use the same iterator mechanisms
as the collection class discussed above. In fact,
there is the Tale\CollectionInterface
they all implement and all of
them use a common base class Tale\AbstractCollection
that defines
most of their APIs.
The Set class is an abstraction for a set of values where keys don't matter. The keys of a set will be managed and always stay sequential.
The values will be unique. If a value already exists in the set,
it won't be added again when using add
.
The content is managed via the following API:
Checks if the set contains a specific item
Adds a new item to the set
Removes an item from the set
use function Tale\set;
$existingObject = new SomeClass();
$set = set();
$set->add(new SomeOtherClass());
var_dump($set->has($existingObject)); //bool(true)
var_dump($set->toArray());
/*
array(3) {
0 => object(SomeOtherClass)#1 (0) {}
1 => object(SomeClass)#2 (0) {}
}
*/
A set of CSS classes on elements
use function Tale\set;
$classList = set(['btn']);
if ($primary) {
$classList->add('btn-primary');
}
if ($block) {
$classList->add('btn-block');
}
if ($classList->has('primary') {
echo "This is a primary button!\n";
}
echo "<button class=\"{$classList->join(' ')}\">My button!</button>";
The map is a data structure that allows using keys other than
strings and integers. Through limitations of the PHP engine,
you can't access complex keys via array access ([$obj]
).
The following API manages a map:
Retrieves the value for a specific key
Sets the value for a specific key
Checks whether a specific key exists or not
Removes a value with a specific key from the map
use function Tale\map;
$key1 = new Key1();
$key2 = new Key2();
$map = map();
$map->set($key1, 'value 1');
$map->set($key2, 'value 2');
var_dump($map->toArray());
/*
array(3) {
0 => array(2) {
0 => object(SomeOtherClass)#1 (0) {}
1 => string(7) "value 1"
}
1 => array(2) {
0 => object(SomeClass)#2 (0) {}
1 => string(7) "value 2"
}
}
*/
A metadata storage for objects
use function Tale\map;
$metadata = map();
$b = new B();
$objects = [
new A(),
$b,
new C(),
new D()
];
foreach ($objects as $obj) {
$metadata->set($obj, getMetadataForObject($obj));
}
//[...]
if ($metadata->has($b)) {
$bMetadata = $metadata->get($b);
//$bMetadata is now the metadata for the $b instance
}
TODO: More docs.