talesoft/tale-collection

dev-master 2018-09-10 19:06 UTC

This package is auto-updated.

Last update: 2020-11-11 06:15:49 UTC


README

Packagist License CI Coverage

Tale Collection

What is Tale Collection?

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.

Installation

composer require talesoft/tale-collection

Usage

Collection

Signature

Tale\collection(iterable $iterable): Tale\CollectionInterface
//or
new Tale\Collection(iterable $iterable)

Examples

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.

Specialized collections

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.

Set

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:

Set->has($item)

Checks if the set contains a specific item

Set->add($item)

Adds a new item to the set

Set->remove($item)

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) {}
}
*/

Common use-cases

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>";

Map

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:

Map->get($key)

Retrieves the value for a specific key

Map->set($key, $value)

Sets the value for a specific key

Map->has($key)

Checks whether a specific key exists or not

Map->remove($key)

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"
  }
}
*/

Common use-cases

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.