improved/iterable

Functions to interact with arrays, iterators and other traversable objects

v0.1.4 2019-06-06 02:25 UTC

This package is auto-updated.

Last update: 2024-11-15 13:07:45 UTC


README

improved PHP library

iterable

PHP Scrutinizer Code Quality Code Coverage Packagist Stable Version Packagist License

Functional-style operations, such as map-reduce transformations on arrays, iterators and other traversable objects.

These functions are different from their array_* counterparts, as they work with any kind of iterable rather than just arrays. If you're not familiar with Iterators and Generators in PHP, please first read paragraph "What are iterators?".

The library supports the procedural and object-oriented programming paradigm.

Installation

composer require improved/iterable

Methods

General methods

Chainable methods

Mapping

Filtering

Sorting

Type handling

Other methods

Finding

Aggregation

Builder methods

Example

All functions and objects are in the Improved namespace. Either alias the namespace as i or import each function individually.

use Improved as i;

$filteredValues = i\iterable_filter($values, function($value) {
   return is_int($value) && $value > 10;
});

$uniqueValues = i\iterable_unique($filteredValues);

$mappedValues = i\iterable_map($uniqueValues, function($value) {
    return $value * $value - 1;
});

$firstValues = i\iterable_slice($mappedValues, 0, 10);

$result = i\iterable_to_array($firstValues);

Alternatively use the iterator pipeline.

use Improved\IteratorPipeline\Pipeline;

$result = Pipeline::with($values)
    ->filter(function($value) {
        return is_int($value) && $value < 10;
    })
    ->unique()
    ->map(function($value) {
        return $value * $value - 1;
    })
    ->limit(10)
    ->toArray();

Usage

This library provides Utility methods for creating streams.

Pipeline takes an array or Traversable object as source argument. The static with() method can be used instead of new.

use Improved\IteratorPipeline\Pipeline;

Pipeline::with([
    new Person("Max", 18),
    new Person("Peter", 23),
    new Person("Pamela", 23)
]);

$dirs = new Pipeline(new \DirectoryIterator('some/path'));

A pipeline uses PHP generators, which are forward-only and non-rewindable. This means a pipeline can only be used one.

PipelineBuilder

The PipelineBuilder can be used to create a blueprint for pipelines. The builder contains the mapping methods of Pipeline and not the other methods.

The static Pipeline::build() method can be used as syntax sugar to create a builder.

use Improved\IteratorPipeline\Pipeline;

$blueprint = Pipeline::build()
    ->checkType('string')
    ->filter(function(string $value): bool) {
        strlen($value) > 10;
    });
    
// later
$pipeline = $blueprint->with($iterable);

A PipelineBuilder is an immutable object, each method call creates a new copy of the builder.

Alternatively the pipeline builder can be invoked, which creates a pipeline and calls toArray() on it.

use Improved\IteratorPipeline\Pipeline;

$unique = Pipeline::build()
    ->unique()
    ->values();

$result = $unique($values);

The then() method can be used to combine two pipeline builder.

use Improved\IteratorPipeline\Pipeline;

$first = Pipeline::build()->unique()->values();
$second = Pipeline::build()->map(function($value) {
    return ucwords($value);
});

$titles = $first->then($second);

$result = $titles($values);

Custom Pipeline class

A Pipeline is not an immutable object, unlike the PipelineBuilder. Only the iterable returned from latest step is relevant and kept by the pipeline. As such, you can extend the Pipeline class and use that any chainable method, without the object changing.

However is a step returns a Pipeline object (including any object that extends the pipeline), the then method will return that object instead of $this. This can be used to inject a custom class later in a pipe or in a pipeline builder.

use Improved\IteratorPipeline\Pipeline;

class MyPipeline extends Pipeline
{
    function product()
    {
        $product = 1;
        
        foreach ($this->iterable as $value) {
            $product *= $value;
        }
        
        return $product;
    }
}
Starting with your custom class
$product = (new MyPipeline)->column('amount')->product();
In an existing pipeline
$pipeline = (new Pipeline)->column('amount');

$product = $pipeline 
    ->then(function(iterable $iterable) {
        return new MyPipeline($iterable);
    })
    ->product();
In a pipeline builder
$builder = (new PipelineBuilder)
    ->then(function(iterable $iterable) {
        return new MyPipeline($iterable);
    })
    ->column('amount')
    ->product();

This is the only way to get a PipelineBuilder to return a custom Pipeline class without also creating a custom PipelineBuilder.

Method reference

then

The then() method defines a new step in the pipeline. As argument it takes a callback that must return a Generator or other Traversable.

Pipeline::with(['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red'])
    ->then(function(\Traversable $values): \Generator {
        foreach ($values as $key => $value) {
            yield $key[0] => "$value $key";
        }
    })
    ->toArray(); // ['a' => 'green apple', 'b' => 'blue berry', 'c' => 'red cherry']

It may be used to apply a custom (outer) iterator.

Pipeline::with(['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red'])
    ->then(function(\Traversable $values): \Iterator {
        return new MyCustomIterator($values);
    });

getIterator

The Pipeline implements the IteratorAggregate interface. This means it's traversable. Alternatively you can use getIterator().

toArray

Copy the elements of the iterator into an array.

Pipeline::with(["one", "two", "three"])
    ->toArray();

walk

Traverse over the iterator, not capturing the values. This is particularly useful after apply().

Pipeline::with($objects)
    ->apply(function($object, $key) {
        $object->type = $key;
    })
    ->walk();

Mapping

map

Map each element to a value using a callback function.

Pipeline::with([3, 2, 2, 3, 7, 3, 6, 5])
    ->map(function(int $i): int {
        return $i * $i;
    })
    ->toArray(); // [9, 4, 4, 9, 49, 9, 36, 25]

The second argument of the callback is the key.

Pipeline::with(['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red'])
    ->map(function(string $value, string $key): string {
        return "{$value} {$key}";
    })
    ->toArray(); // ['apple' => 'green apple', 'berry' => 'blue berry', 'cherry' => 'red cherry']

mapKeys

Map the key of each element to a new key using a callback function.

The second argument of the callback is the value and second is the key.

Pipeline::with(['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red'])
    ->mapKeys(function(string $value, string $key): string {
        return subst($key, 0, 1);
    })
    ->toArray(); // ['a' => 'green', 'b' => 'blue', 'c' => 'red']

apply

Apply a callback to each element of an iterator. Any value returned by the callback is ignored.

$persons = [
    'client' => new Person("Max", 18),
    'seller' => new Person("Peter", 23),
    'lawyer' => new Person("Pamela", 23)
];

Pipeline::with($persons)
    ->apply(function(Person $value, string $key): void {
        $value->role = $key;
    })
    ->toArray();

chunk

Divide iterable into chunks of specified size.

Pipeline::with(['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'])
    ->chunk(4);
    
// <iterator>[
//     <iterator>['I', 'II', 'III', 'IV'],
//     <iterator>['V', 'VI', 'VII', 'VIII'],
//     <iterator>['IX', 'X']
// ]

Chunks are iterators rather than arrays. Keys are preserved.

group

Group elements of an iterator, with the group name as key and an array of elements as value.

Pipeline::with(['apple', 'berry', 'cherry', 'apricot'])
    ->group(function(string $value): string {
        return $value[0];
    })
    ->toArray();
    
// ['a' => ['apple', 'apricot'], 'b' => ['berry'], 'c' => ['cherry']]

The second argument is the key.

Pipeline::with(['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red', 'apricot' => 'orange'])
    ->group(function(string $value, string $key): string {
        return $key[0];
    })
    ->toArray();

// ['a' => ['apple' => 'green', 'apricot' => 'orange'], 'b' => ['berry' => 'blue'], 'c' => ['cherry' => 'red']]

flatten

Walk through all sub-iterables and concatenate them.

$groups = [
    ['one', 'two'],
    ['three', 'four', 'five'],
    [],
    ['six'],
    'seven'
];

Pipeline::with($groups)
    ->flatten()
    ->toArray(); // ['one', 'two', 'three', 'four', 'five', 'six', 'seven']

By default the keys are dropped, replaces by an incrementing counter (so as an numeric array). By passing true as second parameters, the keys are remained.

unwind

Deconstruct an iterable property/item for each element. The result is one element for each item in the iterable property. You must specify which column to unwind.

$elements = [
    ['ref' => 'a', 'numbers' => ['I' => 'one', 'II' => 'two']],
    ['ref' => 'b', 'numbers' => 'three'],
    ['ref' => 'c', 'numbers' => []]
];

Pipeline::with($elements)
    ->unwind('numbers')
    ->toArray();
    
// [
//     ['ref' => 'a', 'numbers' => 'one'],
//     ['ref' => 'a', 'numbers' => 'two'],
//     ['ref' => 'b', 'numbers' => 'three'],
//     ['ref' => 'c', 'numbers' => null]
// ]

The second argument is optional, taking a column name to add each key to the element.

Pipeline::with($elements)
    ->unwind('numbers', 'nrkey')
    ->toArray();
    
// [
//     ['ref' => 'a', 'numbers' => 'one', 'nrkey' => 'I'],
//     ['ref' => 'a', 'numbers' => 'two', 'nrkey' => 'II],
//     ['ref' => 'b', 'numbers' => 'three', 'nrkey' => null],
//     ['ref' => 'c', 'numbers' => null, 'nrkey' => null]
// ]

By default each new element of the resulting iterator is a numeric sequence. To preverse the keys, pass true as third argument. Beware that this will result in duplicate keys.

fill

Set all values of the iterable. Don't touch the keys.

This can be used in combination with flip to something similar to array_fill_keys.

$fields = ['foo', 'bar', 'qux'];

Pipeline::with($fields)
    ->flip()
    ->fill(42)
    ->toArray(); // ['foo' => 42, 'bar' => 42, 'qux' => 42]

column

Return the values from a single column / property. Each element should be an array or object.

$rows = [
    ['one' => 'uno', 'two' => 'dos', 'three' => 'tres', 'four' => 'cuatro', 'five' => 'cinco'],
    ['one' => 'yi', 'two' => 'er', 'three' => 'san', 'four' => 'si', 'five' => 'wu'],
    ['one' => 'één', 'two' => 'twee', 'three' => 'drie', 'five' => 'vijf']
];

Pipeline::with($rows)
    ->column('three')
    ->toArray(); // ['tres', 'san', 'drie']

Create key/value pairs by specifying the key.

$rows = [
    ['one' => 'uno', 'two' => 'dos', 'three' => 'tres', 'four' => 'cuatro', 'five' => 'cinco'],
    ['one' => 'yi', 'two' => 'er', 'three' => 'san', 'four' => 'si', 'five' => 'wu'],
    ['one' => 'één', 'two' => 'twee', 'three' => 'drie', 'five' => 'vijf']
];

Pipeline::with($rows)
    ->column('three', 'two')
    ->toArray(); // ['dos' => 'tres', 'er' => 'san', 'twee' -=> 'drie']

Alternatively you may only specify the key column, using null for the value column, to keep the value unmodified.

If an element doesn't have a specified key, the key and/or value will be null.

project

Project each element of an iterator to an associated (or numeric) array. Each element should be an array or object.

For the projection, a mapping [new key => old key] must be supplied.

$rows = [
    ['one' => 'uno', 'two' => 'dos', 'three' => 'tres', 'four' => 'cuatro', 'five' => 'cinco'],
    ['one' => 'yi', 'two' => 'er', 'three' => 'san', 'four' => 'si', 'five' => 'wu'],
    ['one' => 'één', 'two' => 'twee', 'three' => 'drie', 'five' => 'vijf']
];

Pipeline::with($rows)
    ->project(['I' => 'one', 'II' => 'two', 'II' => 'three', 'IV' => 'four'])
    ->toArray();

// [
//   ['I' => 'uno', 'II' => 'dos', 'III' => 'tres', 'IV' => 'cuatro'],
//   ['I' => 'yi', 'II' => 'er', 'III' => 'san', 'IV' => 'si'],
//   ['I' => 'één', 'II' => 'twee', 'III' => 'drie', 'IV' => null]
// ]

If an element doesn't have a specified key, the value will be null.

The order of keys of the projected array is always the same as the order of the mapping. The mapping may also be a numeric array.

reshape

Reshape each element of an iterator, adding or removing properties or keys.

The method takes an array with the column name as key. The value may be a boolean, specifying if th column should remain or be removed. Alternatively the column may be a string or int, renaming the column name (key).

Columns that are not specified are untouched. This has the same effect as 'column' => true.

$rows = [
    ['one' => 'uno', 'two' => 'dos', 'three' => 'tres', 'four' => 'cuatro', 'five' => 'cinco'],
    ['three' => 'san', 'two' => 'er', 'five' => 'wu', 'four' => 'si'],
    ['two' => 'twee', 'four' => 'vier']
];

Pipeline::with($rows)
    ->reshape(['one' => true, 'two' => false, 'three' => 'III', 'four' => 0])
    ->toArray();

// [
//     ['one' => 'uno', 'five' => 'cinco', 'III' => 'tres', 0 => 'cuatro'],
//     ['five' => 'wu', 'III' => 'san', 0 => 'si'],
//     [0 => 'vier']
// ];

Note that unlike with project(), the array or object is modified. If the element does not have the specific key, it's ignored. If the element is not an object or array, it's untouched.

values

Keep the values, drop the keys. The keys become an incremental number. This is comparable to array_values.

Pipeline::with(['I' => 'one', 'II' => 'two', 'III' => 'three', 'IV' => 'four'])
    ->values()
    ->toArray(); // ['one', 'two', 'three', 'four']

keys

Use the keys as values. The keys become an incremental number. This is comparable to array_keys.

Pipeline::with(['I' => 'one', 'II' => 'two', 'III' => 'three', 'IV' => 'four'])
    ->keys()
    ->toArray(); // ['I', 'II', 'III', 'IV']

setKeys

Use another iterator as keys and the current iterator as values.

Pipeline::with(['one', 'two', 'three', 'four'])
    ->setKeys(['I', 'II', 'III', 'IV'])
    ->toArray(); // ['I' => 'one', 'II' => 'two', 'III' => 'three', 'IV' => 'four']

The key may be any type and doesn't need to be unique.

The number of elements yielded from the iterator only depends on the number of keys. If there are more keys than values, the value defaults to null. If there are more values than keys, the additional values are not returned.

flip

Use values as keys and visa versa.

Pipeline::with(['one' => 'uno', 'two' => 'dos', 'three' => 'tres', 'four' => 'cuatro'])
    ->flip()
    ->toArray(); // ['uno' => 'one', 'dos' => 'two', 'tres' => 'three', 'cuatro' => 'four']

Both the value and key may be any type and don't need to be unique.

Filtering

filter

Eliminate elements based on a criteria.

The callback function is required and should return a boolean.

Pipeline::with([3, 2, 2, 3, 7, 3, 6, 5])
    ->filter(function(int $i): bool {
        return $i % 2 === 0; // is even
    })
    ->toArray(); // [1 => 2, 2 => 2, 6 => 6]

The second argument of the callback is the key.

Pipeline::with(['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red', 'apricot' => 'orange'])
    ->filter(function(string $value, string $key): bool {
        return $key[0] === 'a';
    })
    ->toArray(); // ['apple' => 'green', 'apricot' => 'orange']

cleanup()

Filter out null values or null keys from iterable.

Pipeline::with(['one', 'two', null, 'four', null])
    ->cleanup()
    ->toArray();

// [0 => 'one', 1 => 'two', 3 => 'four']

With iterators, keys may be of any type. Elements with null keys are also filtered out.

unique

Filter on unique elements.

Pipeline::with(['foo', 'bar', 'qux', 'foo', 'zoo'])
    ->unique()
    ->toArray(); // [0 => 'foo', 1 => 'bar', 2 => qux, 4 => 'zoo']

You can pass a callback, which should return a value. Filtering on distinct values will be based on that value.

$persons = [
    new Person("Max", 18),
    new Person("Peter", 23),
    new Person("Pamela", 23)
];

Pipeline::with($persons)
    ->unique(function(Person $value): int {
        return $value->age;
    })
    ->toArray();

// [0 => Person {'name' => "Max", 'age' => 18}, 1 => Person {'name' => "Peter", 'age' => 23}]

All values are stored for reference. The callback function can also be used to serialize and hash the value.

Pipeline::with($persons)
    ->unique(function(Person $value): int {
        return hash('sha256', serialize($value));
    });
});

The seconds argument is the key.

Pipeline::with(['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red', 'apricot' => 'orange'])
    ->unique(function(string $value, string $key): string {
        return $key[0];
    })
    ->toArray(); // ['apple' => 'green', 'berry' => 'blue', 'cherry' => 'red']

Uses strict comparison (===), so '10' and 10 won't match.

uniqueKeys

The keys of an iterator don't have to be unique (and don't have to be a scalar). This is unlike an associated array.

The uniqueKeys() method filters our duplicate keys.

$someGenerator = function($max) {
    for ($i = 0; $i < $max; $i++) {
        $key = substr(md5((string)$i), 0, 1); // char [0-9a-f]
        yield $key => $i;
    }
};

Pipeline::with($someGenerator(1000))
    ->uniqueKeys()
    ->toArray();

// ['c' => 0, 'e' => 3, 'a' => 4, 1 => 6, 8 => 7, 4 => 9, 'd' => 10, 6 => 11 9 => 15 7 => 17,
//     3 => 21, 'b' => 22, 0 => 27, 'f' => 44, 2 => 51, 5 => 91]

limit

Get only the first elements of an iterator.

Pipeline::with([3, 2, 2, 3, 7, 3, 6, 5])
    ->limit(3)
    ->toArray(); // [3, 2, 2]

slice

Get a limited subset of the elements using an offset.

Pipeline::with([3, 2, 2, 3, 7, 3, 6, 5])
    ->slice(3)
    ->toArray(); // [3, 7, 3, 6, 5]

You may also specify a limit.

Pipeline::with([3, 2, 2, 3, 7, 3, 6, 5])
    ->slice(3, 2)
    ->toArray(); // [3, 7]

before

Get elements until a match is found.

Pipeline::with(['apple' => 'green', 'berry' => 'red', 'cherry' => 'red', 'apricot' => 'orange'])
    ->before(function($value, $key) {
        return $value === 'red';
    })
    ->toArray(); // ['apple' => 'green']

The seconds argument is the key.

Pipeline::with(['apple' => 'green', 'berry' => 'red', 'cherry' => 'red', 'apricot' => 'orange'])
    ->before(function($value, $key) {
        return $key === 'berry';
    })
    ->toArray(); // ['apple' => 'green']

Optionally the matched value can be included in the result

Pipeline::with(['apple' => 'green', 'berry' => 'red', 'cherry' => 'red', 'apricot' => 'orange'])
    ->before(function($value) {
        return $value === 'red';
    })
    ->toArray(); // ['apple' => 'green', 'berry' => 'red']

after

Get elements after a match is found.

Pipeline::with(['apple' => 'green', 'berry' => 'red', 'cherry' => 'red', 'apricot' => 'orange'])
    ->before(function($value, $key) {
        return $value === 'red';
    })
    ->toArray(); // ['cherry' => 'red', 'apricot' => 'orange']

The seconds argument is the key.

Pipeline::with(['apple' => 'green', 'berry' => 'red', 'cherry' => 'red', 'apricot' => 'orange'])
    ->before(function($value, $key) {
        return $key === 'berry';
    })
    ->toArray(); // ['cherry' => 'red', 'apricot' => 'orange']

Optionally the matched value can be included in the result

Pipeline::with(['apple' => 'green', 'berry' => 'red', 'cherry' => 'red', 'apricot' => 'orange'])
    ->before(function($value) {
        return $value === 'red';
    })
    ->toArray(); // ['berry' => 'red', 'cherry' => 'red', 'apricot' => 'orange']

Sorting

Sorting requires traversing through the iterator to index all elements.

sort

Create an iterator with sorted elements.

Pipeline::with(["Charlie", "Echo", "Bravo", "Delta", "Foxtrot", "Alpha"])
    ->sort()
    ->toArray(); // ["Alpha", "Beta", "Charlie", "Delta", "Echo", "Foxtrot"]

Instead of using the default sorting, a callback may be passed as user defined comparison function.

Pipeline::with(["Charlie", "Echo", "Bravo", "Delta", "Foxtrot", "Alpha"])
    ->sort(function($a, $b): int {
        return strlen($a) <=> strlen($b) ?: $a <=> $b;
    })
    ->toArray(); // ["Echo", "Alpha", "Bravo", "Delta", "Charlie", "Foxtrot"]

The callback must return < 0 if str1 is less than str2; > 0 if str1 is greater than str2, and 0 if they are equal.

sortKeys

Create an iterator with sorted elements by key.

Pipeline::with(["Charlie" => "three", "Bravo" => "two", "Delta" => "four", "Alpha" => "one"])
    ->sortKeys()
    ->toArray();
    
// ["Alpha" => "one", "Bravo" => "two", "Charlie" => "three", "Delta" => "four"]

A callback may be passed as user defined comparison function.

Pipeline::with(["Charlie" => "three", "Bravo" => "two", "Delta" => "four", "Alpha" => "one"])
    ->sortKeys(function($a, $b): int {
        return strlen($a) <=> strlen($b) ?: $a <=> $b;
    })
    ->toArray(); 
    
// ["Alpha" => "one", "Bravo" => "two", "Delta" => "four", "Charlie" => "three"]

reverse

Create an iterator with elements in the reversed orderd. The keys are preserved.

Pipeline::with(range(5, 10))
    ->reverse()
    ->toArray(); // [5 => 10, 4 => 9, 3 => 8, 2 => 7, 1 => 6, 0 => 5]

Type handling

typeCheck

Validate that a value has a specific type using type_check. A TypeError is thrown if any element of the iterable doesn't match the type.

Pipeline::with($values)
    ->typeCheck(['int', 'float'])
    ->toArray();

As type you may specific any PHP type, a pseudo types like iterable or callable, as class name or a resource type. For resources use the resource type, plus "resource", eg "stream resource".

As second argument, a Throwable object may be passed, this is either an Exception or Error.

The error message may contain up to three sprintf place holders. The first %s is replaced with the type of the value. The second is used for the description of the key. The third is typically not needed, but when specified is replaced with the given type(s).

Pipeline::with($values)
    ->expectType('int', new \UnexpectedValue('Element %2$s should be an integer, %1$s given'))
    ->toArray();

A question mark can be added to a class to accept null, eg "?string" is similar to using ["string", "null"].

typeCast

Cast a value to the specific type. This method uses type_cast.

If the value can't be cast, a TypeError is thrown. Similar to typeCheck() a Throwable with a message may be passed as second argument.

In contrary typeCheck, only one type may be specified. A question mark can be added to a class to accept null, eg ?string will try to cast everything to a string except null.

Finding

These methods invoke traversing through the iterator and return a single element.

first

Get the first element.

Pipeline::with(["one", "two", "three"])
    ->first(); // "one"

Optionally a RangeException can be thrown if the iterable is empty.

last

Get the last element.

Pipeline::with(["one", "two", "three"])
    ->last(); // "three"

find

Find the first element that matches a condition. Returns null if no element is found.

Pipeline::with(["one", "two", "three"])
    ->find(function(string $value): bool {
        return substr($value, 0, 1) === 't';
    }); // "two"

It's possible to use the key in this callable.

Pipeline::with(["one" => "uno", "two" => "dos", "three" => "tres"])
    ->find(function(string $value, string $key): bool {
        return substr($key, 0, 1) === 't';
    }); // "dos"

findKey

Find the first element that matches a condition and return the key (rather than the value). Returns null if no element is found.

Pipeline::with(["I" => "one", "II" => "two", "III" => "three"])
    ->find(function(string $value): bool {
        return substr($value, 0, 1) === 't';
    }); // "II"

It's possible to use the key in this callable.

Pipeline::with(["one" => "uno", "two" => "dos", "three" => "tres"])
    ->find(function(string $value, string $key): bool {
        return substr($key, 0, 1) === 't';
    }); // "two"

hasAny

Check if any element matches the given condition.

Pipeline::with(["one", "two", "three"])
    ->hasAny(function(string $value): bool {
        return substr($value, 0, 1) === 't';
    }); // true

The callback is similar to find.

hasAll

Check if all elements match the given condition.

Pipeline::with(["one", "two", "three"])
    ->hasAny(function(string $value): bool {
        return substr($value, 0, 1) === 't';
    }); // false

The callback is similar to find.

hasNone

Check the no element matches the given condition. This is the inverse of hasAny().

Pipeline::with(["one", "two", "three"])
    ->hasNone(function(string $value): bool {
        return substr($value, 0, 1) === 't';
    }); // false

The callback is similar to find.

min

Returns the minimal element according to a given comparator.

Pipeline::with([99.7, 24, -7.2, -337, 122.0]))
    ->min(); // -337

It's possible to pass a callable for custom logic for comparison.

Pipeline::with([99.7, 24, -7.2, -337, 122.0])
    ->min(function($a, $b) {
        return abs($a) <=> abs($b);
    }); // -7.2

max

Returns the maximal element according to a given comparator.

Pipeline::with([99.7, 24, -7.2, -337, 122.0]))
    ->max(); // 122.0

It's possible to pass a callable for custom logic for comparison.

Pipeline::with([99.7, 24, -7.2, -337, 122.0])
    ->max(function($a, $b) {
        return abs($a) <=> abs($b);
    }); // -337

Aggregation

Traverse through all elements and reduce it to a single value.

count

Returns the number of elements.

Pipeline::with([2, 8, 4, 12]))
    ->count(); // 4

reduce

Reduce all elements to a single value using a callback.

Pipeline::with([2, 3, 4])
    ->reduce(function(int $product, int $value): int {
        return $product * $value;
    }, 1); // 24

The third argument is the key

Pipeline::with(['I' => 'one, 'II' => 'two', 'III' => 'three'])
    ->reduce(function(string $list, string $value, string $key): string {
        return $list . sprintf("{%s:%s}", $key, $value);
    }, ''); // "{I:one}{II:two}{III:three}"

sum

Calculate the sum of a numbers. If no elements are present, the result is 0.

Pipeline::with([2, 8, 4, 12])
    ->sum(); // 26

average

Calculate the arithmetic mean. If no elements are present, the result is NAN.

Pipeline::with([2, 8, 4, 12]))
    ->average(); // 6.5

concat

Concatenate the input elements, separated by the specified delimiter, in encounter order.

This is comparable to implode on normal arrays.

Pipeline::with(["hello", "sweet", "world"])
    ->concat(" - "); // "hello - sweet - world"

stub

The stub() method a stub step, which does nothing but can be replaced later using unstub().

PipelineBuilder stub(string name)
PipelineBuilder unstub(string name, callable $callable, mixed ...$args) 

These methods only exists in the pipeline builder.

$blueprint = Pipeline::build()
    ->expectType('string')
    ->stub('process');
    ->sort();
    
// Later
$pipeline = $blueprint
    ->unstub('process', i\iterable_map, i\function_partial(i\string_convert_case, ___, i\STRING_UPPERCASE)));

What are iterators?

Iterators are traversable objects. That means that when you use them in a foreach loop, you're not looping through the properties of the object. Instead the current(), key() and valid() methods are called each time we go through the loop.

The current() method gives the current value, the key() method gives the current key and the valid() method checks if we're should continue looping.

In the following example we extend IteratorIterator to overwrite the current() class.

use Improved as i;

class UpperIterator extends IteratorIterator
{
    public function current()
    {
        return i\string_case_convert(parent::current(), i\STRING_UPPERCASE);
    }
};

class NoSpaceIterator extends IteratorIterator
{
    public function current()
    {
        return i\string_replace(parent::current(), " ", "");
    }
};

$data = get_some_data();
$iterator = new NoSpaceIterator(new UpperIterator(new ArrayIterator($data)));

At this point nothing is executed. Neither string_case_convert or string_replace. Only once we loop, these functions are called.

foreach ($iterator as $cleanValue) {
    echo $cleanValue;
}

This will be the same as doing

foreach ($data as $value) {
    $upperValue = i\string_case_convert($value, i\STRING_UPPERCASE);
    $cleanValue = i\string_replace($upper, " ", "");
    
    echo $cleanValue;
}

Difference to working with arrays

When working with arrays, we tend to loop through them for each operation. Look at array_map

$upperData = array_map(function($value) {
    return i\string_case_convert($value, i\STRING_UPPERCASE);
}, $data);

$cleanData = array_map(function($value) {
    return i\string_replace($value, i\STRING_UPPERCASE);
}, $upperData);

foreach ($cleanData as $cleanValue) {
    echo $cleanValue;
}

Which is similar to

$upperData = [];
$cleanData = [];

foreach ($data as $value) {
    $upperData[] = i\string_case_convert($value, i\STRING_UPPERCASE);
}

foreach ($upperData as $upperValue) {
    $cleanData[] = i\string_replace($upperValue, i\STRING_UPPERCASE);
}

foreach ($cleanData as $cleanValue) {
    echo $cleanValue;
}

Of course we could combine these operators an apply them in a simple loop without the use of iterators. However this couples all that logic. If a method returns all values in upper case, a second and unrelated method (in a different class) might remove the spaces. For iterators this doesn't matter.

Iterator keys

With iterators, the key doesn't need to be a string or integer, but can be any type and doesn't need to be unique.

It can very convenient to make the key an array or object and keeping the value a scalar. As such you can do link operations like case conversion, etc. Another application is to group child objects per parent object.

Generators

Generators are special iterators which are automatically created by PHP when you use the yield syntax.

function iterable_first_word(iterable $values): Generator
{
    foreach ($values as $key => $value) {
        $word = i\string_before($value, " ");
    
        yield $key => $word;
    }
}

PHP 7.2+ is highly optimized to work with generators increasing performance and saving memory. This makes generators preferable to custom iterators, which can be slow.

Unexpected generator behaviour

If you add a return statement, the function will still return a Generator object. You can get that result with the Generator->getReturn() method, but this is typically not what's intended.

function get_values(iterable $values)
{
    if (is_array($values)) {
        return array_values($values);
    }

    foreach ($values as $value) {
        yield $value;
    }
}

The following code will not work as intended. It will not return an array, but always a Generator object.

Also note that the none of the code in the get_values function will execute until the we start the loop

function iterable_first_word(iterable $values): Generator
{
    var_dump($values);

    foreach ($values as $key => $value) {
        yield $key => i\string_before($value, " ");
    }
}

$words = iterable_first_word($values);

// Nothing is outputted yet

foreach ($words as $word) { // Now we get the var_dump() as the function is executed till yield 
    // ...
}

Forward-only iterators

Some iterators, including generators, are forward-only iterators, meaning you can only loop through them once.

function numbers_to($count) {
    for ($i = 1; $i <= $count; $i++) {
        yield $i;
    }
}

$oneToTen = numbers_to(10);

foreach ($oneToTen as $number) {
    echo $number;
}

// The following loop will cause an error to be thrown.

foreach ($oneToTen as $number) {
    foo($number);
}

This has consequences when using the iterable_ functions and Pipeline objects. Though this can be overcome using a PipelineBuilder.

And now you know :-)