Skip to content
A (memory) friendly, lazy, easy and modular collection class.
PHP
Branch: master
Clone or download
Latest commit 3dd5d4a Sep 17, 2019

README.md

Latest Stable Version GitHub stars Total Downloads Build Status Scrutinizer code quality Code Coverage Mutation testing badge License Say Thanks! Donate!

PHP Collection

Description

Collection is a functional utility library for PHP.

It's similar to other available collection libraries based on regular PHP arrays, but with a lazy mechanism under the hood that strives to do as little work as possible while being as flexible as possible.

Collection leverages PHP's generators and iterators to allow you to work with very large data sets while keeping memory usage as low as possible.

For example, imagine your application needs to process a multi-gigabyte log file while taking advantage of this library's methods to parse the logs. Instead of reading the entire file into memory at once, this library may be used to keep only a small part of the file in memory at a given time.

On top of this, this library:

Except a few methods, most of methods are pure and return a new Collection object.

This library has been inspired by the Laravel Support Package and Lazy.js.

Requirements

  • PHP >= 7.1.3

Installation

It has no external dependencies, so you can get started right away with:

composer require drupol/collection

Usage

<?php

declare(strict_types=1);

include 'vendor/autoload.php';

use drupol\collection\Collection;

// More examples...
$collection = Collection::with(['A', 'B', 'C', 'D', 'E']);

// Get the result as an array.
$collection
    ->all(); // ['A', 'B', 'C', 'D', 'E']

// Get the first item.
$collection
    ->first(); // A

// Append items.
$collection
    ->append('F', 'G', 'H')
    ->all(); // ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']

// Prepend items.
$collection
    ->prepend('1', '2', '3')
    ->all(); // ['1', '2', '3', 'A', 'B', 'C', 'D', 'E']

// Split a collection into chunks of a given size.
$collection
    ->chunk(2)
    ->map(static function (Collection $collection) {return $collection->all();})
    ->all(); // [['A', 'B'], ['C', 'D'], ['E']]

// Merge items.
$collection
    ->merge([1, 2], [3, 4], [5, 6])
    ->all(); // ['A', 'B', 'C', 'D', 'E', 1, 2, 3, 4, 5, 6]

// Map data
$collection
    ->map(
        static function ($value, $key) {
            return sprintf('%s.%s', $value, $value);
        }
    )
    ->all(); // ['A.A', 'B.B', 'C.C', 'D.D', 'E.E']

// ::map() and ::walk() are not the same.
Collection::with(['A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E'])
    ->map(
        static function ($value, $key) {
            return strtolower($value);
        }
    )
    ->all(); // [0 => 'a', 1 => 'b', 2 => 'c', 3 = >'d', 4 => 'e']

Collection::with(['A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E'])
    ->walk(
        static function ($value, $key) {
            return strtolower($value);
        }
    )
    ->all(); // ['A' => 'a', B => 'b', 'C' => 'c', 'D' = >'d', 'E' => 'e']

// Infinitely loop over numbers, square them, filter those that are not divisible by 5, take the first 100 of them.
Collection::range(0, INF)
    ->map(
        static function ($value, $key) {
            return $value ** 3;
        }
    )
    ->filter(
        static function ($value, $key) {
            return $value % 5;
        }
    )
    ->limit(100)
    ->all(); // [1, 8, 27, ..., 1815848, 1860867, 1906624]

// Apply a callback to the values without altering the original object.
// If the callback returns false, then it will stop.
Collection::with(range('A', 'Z'))
    ->apply(
        static function ($value, $key) {
            echo strtolower($value);
        }
    );

// Generate 300 distinct random numbers between 0 and 1000
$random = static function() {
    return mt_rand() / mt_getrandmax();
};

Collection::iterate($random)
    ->map(
        static function ($value) {
            return floor($value * 1000) + 1;
        }
    )
    ->distinct()
    ->limit(300)
    ->normalize()
    ->all();

// The famous Fibonacci example:
Collection::with(
        static function($start = 0, $inc = 1) {
            yield $start;

            while(true)
            {
                $inc = $start + $inc;
                $start = $inc - $start;
                yield $start;
            }
        }
    )
    ->limit(10)
    ->all(); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// Fibonacci using the static method ::iterate()
Collection::iterate(
    static function($previous, $next) {
        return [$next, $previous + $next];
    },
    1,1
    )
    // Get the first item of each result.
    ->pluck(0)
    // Limit the amount of results to 10.
    ->limit(10)
    // Convert to regular array.
    ->all(); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

// Find the Golden Ratio with Fibonacci
$fibonacci = Collection::iterate(
    static function($previous, $next) {
        return [$next, $previous + $next];
    },
    1,1
    );

Collection::with($fibonacci)
    ->map(
        static function(array $value, $key) {
            [$previous, $next] = $value;

            return $next / $previous;
        }
    )
    ->limit(100)
    ->last(); // 1.6180339887499

// Use an existing Generator as input data.
$readFileLineByLine = static function (string $filepath): Generator {
    $fh = \fopen($filepath, 'rb');

    while (false !== $line = fgets($fh)) {
        yield $line;
    }

    \fclose($fh);
};

$hugeFile = __DIR__ . '/vendor/composer/autoload_static.php';

Collection::with($readFileLineByLine($hugeFile))
    // Add the line number at the end of the line, as comment.
    ->map(
        static function ($value, $key) {
            return str_replace(PHP_EOL, ' // line ' . $key . PHP_EOL, $value);
        }
    )
    // Find public static fields or methods among the results.
    ->filter(
        static function ($value, $key) {
            return false !== strpos(trim($value), 'public static');
        }
    )
    // Skip the first result.
    ->skip(1)
    // Limit to 3 results only.
    ->limit(3)
    // Implode into a string.
    ->implode();

// Load a string
$string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  Quisque feugiat tincidunt sodales. 
  Donec ut laoreet lectus, quis mollis nisl. 
  Aliquam maximus, orci vel placerat dapibus, libero erat aliquet nibh, nec imperdiet felis dui quis est. 
  Vestibulum non ante sit amet neque tincidunt porta et sit amet neque. 
  In a tempor ipsum. Duis scelerisque libero sit amet enim pretium pulvinar. 
  Duis vitae lorem convallis, egestas mauris at, sollicitudin sem. 
  Fusce molestie rutrum faucibus.';

// By default will have the same behavior as str_split().
Collection::with($string)
    ->explode(' ')
    ->count(); // 71

// Or add a separator if needed, same behavior as explode().
Collection::with($string, ',')
  ->count(); // 9

// The Collatz conjecture (https://en.wikipedia.org/wiki/Collatz_conjecture)
$collatz = static function (int $initial = 1): int
{
    return 0 === $initial % 2 ?
        $initial / 2:
        $initial * 3 + 1;
};

Collection::iterate($collatz, 10)
    ->until(static function ($number): bool {
        return 1 === $number;
    })
    ->all(); // [5, 16, 8, 4, 2, 1]


Advanced usage

You can choose to build your own collection object by extending the Base collection object or by just creating a new Operation.

Each already existing operations of the Collection class live in its own class file.

In order to extend the Collection features, create your own custom operation by creating an object implementing the Operation interface, then run it through the Collection::run() method.

<?php

declare(strict_types=1);

use drupol\collection\Collection;
use drupol\collection\Contract\Operation;

include 'vendor/autoload.php';

$square = new class implements Operation {
    /**
     * {@inheritdoc}
     */
    public function on(iterable $iterable): \Closure
    {
        return static function () use ($iterable) {
            foreach ($iterable as $value) {
                yield $value ** 2;
            }
        };
    }
};

Collection::with(
    Collection::range(5, 15)
        ->run($square)
)->all();

Another way would be to create your own custom collection object:

In the following example and just for the sake of creating an example, the custom collection object will only be able to transform any input (iterable or \Generator) into a regular array.

<?php

declare(strict_types=1);

include 'vendor/autoload.php';

use drupol\collection\Base;
use drupol\collection\Contract\Allable;
use drupol\collection\Contract\Runable;
use drupol\collection\Transformation\All;
use drupol\collection\Transformation\Run;
use drupol\collection\Contract\Operation;

$customCollectionClass = new class extends Base implements Allable {

    /**
     * {@inheritdoc}
     */
    public function all(): array {
        return $this->run(new All());
    }
};

$customCollection = new $customCollectionClass(new ArrayObject(['A', 'B', 'C']));

print_r($customCollection->all()); // ['A', 'B', 'C']

$generator = function() {
  yield 'A';
  yield 'B';
  yield 'C';
};

$customCollection = new $customCollectionClass($generator);

print_r($customCollection->all()); // ['A', 'B', 'C']

The Collection object implements all the interfaces from this library, and is set as final. Use it like it is, decorated it or create your own object by using the same procedure as shown here.

API

Most of the methods are pure PHP functions, the methods always return the same values for the same inputs.

Regular methods

Methods Return type Source
all array All.php
append new Collection object Append.php
apply new Collection object Apply.php
chunk new Collection object Chunk.php
collapse new Collection object Collapse.php
combine new Collection object Combine.php
count int Count.php
distinct new Collection object Distinct.php
explode new Collection object Explode.php
filter new Collection object Filter.php
first mixed First.php
flatten new Collection object Flatten.php
flip new Collection object Flip.php
forget new Collection object Forget.php
get mixed Get.php
getIterator Iterator Collection.php
implode string Implode.php
intersperse new Collection object Intersperse.php
keys new Collection object Keys.php
last mixed Last.php
limit new Collection object Limit.php
map new Collection object Collection.php
merge new Collection object Merge.php
normalize new Collection object Normalize.php
nth new Collection object Nth.php
only new Collection object Only.php
pad new Collection object Pad.php
pluck new Collection object Pluck.php
prepend new Collection object Prepend.php
rebase new Collection object Collection.php
reduce mixed Reduce.php
reduction new Collection object Reduction.php
run mixed Run.php
skip new Collection object Skip.php
slice new Collection object Slice.php
sort new Collection object Sort.php
split new Collection object Split.php
until new Collection object Until.php
walk new Collection object Walk.php
zip new Collection object Zip.php

All those methods are described in the Collection interface, feel free to check it out for more information about the kind of parameters they require.

Static methods

Methods Return type Source
empty new Collection object Collection.php
iterate new Collection object Collection.php
range new Collection object Collection.php
times new Collection object Collection.php
with new Collection object Collection.php

All those methods are not described in the Collection interface, but in the Collection class itself, feel free to check it out to know more about the kind of parameters they require.

Code style, code quality, tests and benchmarks

The code style is following PSR-12 plus a set of custom rules, the package drupol/php-conventions is responsible for this.

Every time changes are introduced into the library, Travis CI run the tests and the benchmarks.

The library has tests written with PHPSpec. Feel free to check them out in the spec directory. Run composer phpspec to trigger the tests.

PHPInfection is used to ensure that your code is properly tested, run composer infection to test your code.

On the internet

Contributing

See the file CONTRIBUTING.md but feel free to contribute to this library by sending Github pull requests.

You can’t perform that action at this time.