Skip to content

PHP underscore inspired &/or cloned from _.js, with extra goodies like higher order messaging

License

Notifications You must be signed in to change notification settings

adhocore/php-underscore

Repository files navigation

adhocore/underscore

PHP underscore inspired &/or cloned from awesome _.js. A set of utilities and data manipulation helpers providing convenience functionalites to deal with array, list, hash, functions and so on in a neat elegant and OOP way. Guaranteed to save you tons of boiler plate codes when churning complex data collection.

Latest Version Travis Build Scrutinizer CI Codecov branch StyleCI Software License Tweet Support

  • Zero dependency (no vendor bloat).

Installation

Requires PHP5.6 or later.

composer require adhocore/underscore

Usage and API

Although all of them are available with helper function underscore($data) or new Ahc\Underscore($data), the methods are grouped and organized in different heriarchy and classes according as their scope. This keeps it maintainable and saves from having a God class.

Contents


Underscore

constant(mixed $value): callable

Generates a function that always returns a constant value.

$fn = underscore()->constant([1, 2]);

$fn(); // [1, 2]

noop(): void

No operation!

underscore()->noop(); // void/null

random(int $min, int $max): int

Return a random integer between min and max (inclusive).

$rand = underscore()->random(1, 10);

times(int $n, callable $fn): self

Run callable n times and create new collection.

$fn = function ($i) { return $i * 2; };

underscore()->times(5, $fn)->get();
// [0, 2, 4, 6, 8]

uniqueId(string $prefix): string

Generate unique ID (unique for current go/session).

$u  = underscore()->uniqueId();      // '1'
$u1 = underscore()->uniqueId();      // '2'
$u3 = underscore()->uniqueId('id:'); // 'id:3'

UnderscoreFunction

compose(callable $fn1, callable $fn2, ...callable|null $fn3): mixed

Returns a function that is the composition of a list of functions, each consuming the return value of the function that follows.

$c = underscore()->compose('strlen', 'strtolower', 'strtoupper');

$c('aBc.xYz'); // ABC.XYZ => abc.xyz => 7

delay(callable $fn, int $wait): mixed

Cache the result of callback for given arguments and reuse that in subsequent call.

$cb = underscore()->delay(function () { echo 'OK'; }, 100);

// waits 100ms
$cb(); // 'OK'

memoize(callable $fn): mixed

Returns a callable which when invoked caches the result for given arguments and reuses that result in subsequent calls.

$sum = underscore()->memoize(function ($a, $b) { return $a + $b; });

$sum(4, 5); // 9

// Uses memo:
$sum(4, 5); // 9

throttle(callable $fn, int $wait): mixed

Returns a callable that wraps given callable which can be only invoked at most once per given $wait threshold.

$fn = underscore()->throttle($callback, 100);

while (...) {
    $fn(); // it will be constantly called but not executed more than one in 100ms

    if (...) break;
}

UnderscoreArray

compact(): self

Get only the truthy items.

underscore($array)->compact()->get();
// [1 => 'a', 4 => 2, 5 => [1]

difference(array|mixed $data): self

Get the items whose value is not in given data.

underscore([1, 2, 1, 'a' => 3, 'b' => [4]])->difference([1, [4]])->get();
// [1 => 2, 'a' => 3]

findIndex(callable $fn): mixed|null

Find the first index that passes given truth test.

$u = underscore([[1, 2], 'a' => 3, 'x' => 4, 'y' => 2, 'b' => 'B']);

$isEven = function ($i) { return is_numeric($i) && $i % 2 === 0; };

$u->findIndex();        // 0
$u->findIndex($isEven); // 'x'

findLastIndex(callable $fn): mixed|null

Find the last index that passes given truth test.

$u = underscore([[1, 2], 'a' => 3, 'x' => 4, 'y' => 2, 'b' => 'B']);

$isEven = function ($i) { return is_numeric($i) && $i % 2 === 0; };

$u->findLastIndex();        // 'b'
$u->findLastIndex($isEven); // 'y'

first(int $n): array|mixed

Get the first n items.

underscore([1, 2, 3])->first(); // 1
underscore([1, 2, 3])->first(2); // [1, 2]

flatten(): self

Gets the flattened version of multidimensional items.

$u = underscore([0, 'a', '', [[1, [2]]], 'b', [[[3]], 4, 'c', underscore([5, 'd'])]]);

$u->flatten()->get(); // [0, 'a', '', 1, 2, 'b', 3, 4, 'c', 5, 'd']

indexOf(mixed $value): string|int|null

Find the first index of given value if available null otherwise.

$u = underscore([[1, 2], 'a' => 2, 'x' => 4]);

$array->indexOf(2); // 'a'

intersection(array|mixed $data): self

Gets the items whose value is common with given data.

$u = underscore([1, 2, 'a' => 3]);

$u->intersection([2, 'a' => 3, 3])->get(); // [1 => 2, 'a' => 3]

last(int $n): array|mixed

Get the last n items.

underscore([1, 2, 3])->last();   // 3
underscore([1, 2, 3])->last(2);  // [2, 3]

lastIndexOf(mixed $value): string|int|null

Find the last index of given value if available null otherwise.

$u = underscore([[1, 2], 'a' => 2, 'x' => 4, 'y' => 2]);

$array->lastIndexOf(2); // 'y'

object(string|null $className): self

Hydrate the items into given class or stdClass.

underscore(['a', 'b' => 2])->object(); // stdClass(0: 'a', 'b': 2)

range(int $start, int $stop, int $step): self

Creates a new range from start to stop with given step.

underscore()->range(4, 9)->get(); // [4, 5, 6, 7, 8, 9]

sortedIndex(mixed $object, callable|string $fn): string|int|null

Gets the smallest index at which an object should be inserted so as to maintain order.

underscore([1, 3, 5, 8, 11])->sortedIndex(9, null); // 4

union(array|mixed $data): self

Get the union/merger of items with given data.

$u = underscore([1, 2, 'a' => 3]);

$u->union([3, 'a' => 4, 'b' => [5]])->get(); // [1, 2, 'a' => 4, 3, 'b' => [5]]

unique(callable|string $fn): self

Gets the unique items using the id resulted from callback.

$u = underscore([1, 2, 'a' => 3]);

$u->union([3, 'a' => 4, 'b' => [5]])->get();
// [1, 2, 'a' => 4, 3, 'b' => [5]]

zip(array|mixed $data): self

Group the values from data and items having same indexes together.

$u = underscore([1, 2, 'a' => 3, 'b' => 'B']);

$u->zip([2, 4, 'a' => 5])->get();
// [[1, 2], [2, 4], 'a' => [3, 5], 'b' => ['B', null]]

UnderscoreCollection

contains(mixed $item): bool

Check if the collection contains given item.

$u = underscore(['a' => 1, 'b' => 2, 'c' => 3, 5]);

$u->contains(1);   // true
$u->contains('x'); // false

countBy(callable|string $fn): self

Count items in each group indexed by the result of callback.

$u = underscore([
    ['a' => 0, 'b' => 1, 'c' => 1],
    ['a' => true, 'b' => false, 'c' => 'c'],
    ['a' => 2, 'b' => 1, 'c' => 2],
    ['a' => 1, 'b' => null, 'c' => 0],
]);

// by key 'a'
$u->countBy('a')->get();
// [0 => 1, 1 => 2, 2 => 1]

each(callable $fn): self

Apply given callback to each of the items in collection.

$answers = [];
underscore([1, 2, 3])->each(function ($num) use (&$answers) {
    $answers[] = $num * 5;
});

$answers; // [5, 10, 15]

every(callable $fn): bool

Tests if all the items pass given truth test.

$gt0 = underscore([1, 2, 3, 4])->every(function ($num) { return $num > 0; });

$gt0; // true

filter(callable|string|null $fn): self

Find and return all the items that passes given truth test.

$gt2 = underscore([1, 2, 4, 0, 3])->filter(function ($num) { return $num > 2; });

$gt2->values(); // [4, 3]

find(callable $fn, bool $useValue): mixed|null

Find the first item (or index) that passes given truth test.

$num = underscore([1, 2, 4, 3])->find(function ($num) { return $num > 2; });

$num; // 4

$idx = underscore([1, 2, 4, 3])->find(function ($num) { return $num > 2; }, false);

$idx; // 2

findWhere(array $props): mixed

Get the first item that contains all the given props (matching both index and value).

$u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 2], ['a' => 1, 'b' => 3]]);

$u->findWhere(['b' => 3]); // ['a' => 1, 'b' => 3]

groupBy(callable|string $fn): self

Group items by using the result of callback as index. The items in group will have original index intact.

$u = underscore([
    ['a' => 0, 'b' => 1, 'c' => 1],
    ['a' => true, 'b' => false, 'c' => 'c'],
    ['a' => 2, 'b' => 1, 'c' => 2],
    ['a' => 1, 'b' => null, 'c' => 0],
]);

// by key 'a'
$u->groupBy('a')->get();
// [
//  0 => [0 => ['a' => 0, 'b' => 1, 'c' => 1]],
//  1 => [1 => ['a' => true, 'b' => false, 'c' => 'c'], 3 => ['a' => 1, 'b' => null, 'c' => 0]],
//  2 => [2 => ['a' => 2, 'b' => 1, 'c' => 2]],
// ]

indexBy(callable|string $fn): self

Reindex items by using the result of callback as new index.

$u = underscore([
    ['a' => 0, 'b' => 1, 'c' => 1],
    ['a' => true, 'b' => false, 'c' => 'c'],
    ['a' => 2, 'b' => 1, 'c' => 2],
    ['a' => 1, 'b' => null, 'c' => 0],
]);

// by key 'a'
$u->indexBy('a')->get();
// [
//   0 => ['a' => 0, 'b' => 1, 'c' => 1],
//   1 => ['a' => 1, 'b' => null, 'c' => 0],
//   2 => ['a' => 2, 'b' => 1, 'c' => 2],
// ]

invoke(callable $fn): mixed

Invoke a callback using all of the items as arguments.

$sum = underscore([1, 2, 4])->invoke(function () { return array_sum(func_get_args()); });

$sum; // 7

map(callable $fn): self

Update the value of each items with the result of given callback.

$map = underscore([1, 2, 3])->map(function ($num) { return $num * 2; });

$map->get(); // [2, 4, 6]

max(callable|string|null $fn): mixed

Find the maximum value using given callback or just items.

underscore([1, 5, 4])->max(); // 5
$u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]);

$u->max(function ($i) { return $i['a'] + $i['b']; }); // 5

min(callable|string|null $fn): mixed

Find the minimum value using given callback or just items.

underscore([1, 5, 4])->min(); // 1
$u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]);

$u->min(function ($i) { return $i['a'] + $i['b']; }); // 1

partition(callable|string $fn): self

Separate the items into two groups: one passing given truth test and other failing.

$u = underscore(range(1, 10));

$oddEvn = $u->partition(function ($i) { return $i % 2; });

$oddEvn->get(0); // [1, 3, 5, 7, 9]
$oddEvn->get(1); // [2, 4, 6, 8, 10]

pluck(string|int $columnKey, string|int $indexKey): self

Pluck given property from each of the items.

$u = underscore([['name' => 'moe', 'age' => 30], ['name' => 'curly']]);

$u->pluck('name')->get(); // ['moe', 'curly']

reduce(callable $fn, mixed $memo): mixed

Iteratively reduce the array to a single value using a callback function.

$sum = underscore([1, 2, 3])->reduce(function ($sum, $num) {
    return $num + $sum;
}, 0);

$sum; // 6

reduceRight(callable $fn, mixed $memo): mixed

Same as reduce but applies the callback from right most item first.

$concat = underscore([1, 2, 3, 4])->reduceRight(function ($concat, $num) {
    return $concat . $num;
}, '');

echo $concat; // '4321'

reject(callable $fn): self

Find and return all the items that fails given truth test.

$evens = underscore([1, 2, 3, 4, 5, 7, 6])->reject(function ($num) {
    return $num % 2 !== 0;
});

$evens->values(); // [2, 4, 6]

sample(int $n): self

Get upto n items in random order.

$u = underscore([1, 2, 3, 4]);

$u->sample(1)->count(); // 1
$u->sample(2)->count(); // 2

shuffle(): self

Randomize the items keeping the indexes intact.

underscore([1, 2, 3, 4])->shuffle()->get();

some(callable $fn): bool

Tests if some (at least one) of the items pass given truth test.

$some = underscore([1, 2, 0, 4, -1])->some(function ($num) {
    return $num > 0;
});

$some; // true

sortBy(callable $fn): self

Sort items by given callback and maintain indexes.

$u = underscore(range(1, 15))->shuffle(); // randomize
$u->sortBy(null)->get(); // [1, 2, ... 15]

$u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]);
$u->sortBy('a')->get();
// [2 => ['a' => 0, 'b' => 1], 0 => ['a' => 1, 'b' => 2], 1 => ['a' => 2, 'b' => 3]]

$u->sortBy(function ($i) { return $i['a'] + $i['b']; })->get();
// [2 => ['a' => 0, 'b' => 1], 0 => ['a' => 1, 'b' => 2], 1 => ['a' => 2, 'b' => 3]],

where(array $props): self

Filter only the items that contain all the given props (matching both index and value).

$u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 2], ['a' => 1, 'b' => 3, 'c']]);

$u->where(['a' => 1, 'b' => 2])->get(); // [['a' => 1, 'b' => 2, 'c']]

UnderscoreBase

_(array|mixed $data): self

A static shortcut to constructor.

$u = Ahc\Underscore\Underscore::_([1, 3, 7]);

__toString(): string

Stringify the underscore instance as json string.

echo (string) underscore([1, 2, 3]); // [1, 2, 3]
echo (string) underscore(['a', 2, 'c' => 3]); // {0: "a", 1: 2, "c":3}

asArray(mixed $data, bool $cast): array

Get data as array.

underscore()->asArray('one');                        // ['one']
underscore()->asArray([1, 2]);                       // [1, 2]
underscore()->asArray(underscore(['a', 1, 'c', 3])); // ['a', 1, 'c', 3]

underscore()->asArray(new class {
    public function toArray()
    {
        return ['a', 'b', 'c'];
    }
}); // ['a', 'b', 'c']

underscore()->asArray(new class implements \JsonSerializable {
    public function jsonSerialize()
    {
        return ['a' => 1, 'b' => 2, 'c' => 3];
    }
}); // ['a' => 1, 'b' => 2, 'c' => 3]

clon(): self

Creates a shallow copy of itself.

$u = underscore(['will', 'be', 'cloned']);

$u->clon() ==  $u; // true
$u->clon() === $u; // false

count(): int

Gets the count of items.

underscore([1, 2, [3, 4]])->count(); // 3
underscore()->count();               // 0

flat(array $array): array

Flatten a multi dimension array to 1 dimension.

underscore()->flat([1, 2, [3, 4, [5, 6]]]); // [1, 2, 3, 4, 5, 6]

get(string|int|null $index): mixed

Get the underlying array data by index.

$u = underscore([1, 2, 3]);

$u->get();  // [1, 2, 3]
$u->get(1); // 2
$u->get(3); // 3

getData(): array

Get data.

// same as `get()` without args:
underscore([1, 2, 3])->getData(); // [1, 2, 3]

getIterator(): \ArrayIterator

Gets the iterator for looping.

$it = underscore([1, 2, 3])->getIterator();

while ($it->valid()) {
    echo $it->current();
}

invert(): self

Swap index and value of all the items. The values should be stringifiable.

$u = underscore(['a' => 1, 'b' => 2, 'c' => 3]);

$u->invert()->get(); // [1 => 'a', 2 => 'b', 3 => 'c']

jsonSerialize(): array

Gets the data for json serialization.

$u = underscore(['a' => 1, 'b' => 2, 'c' => 3]);

json_encode($u); // {"a":1,"b":2,"c":3}

keys(): self

Get all the keys.

$u = underscore(['a' => 1, 'b' => 2, 'c' => 3, 5]);

$u->keys()->get(); // ['a', 'b', 'c', 0]

mixin(string $name, \Closure $fn): self

Adds a custom handler/method to instance. The handler is bound to this instance.

Ahc\Underscore\Underscore::mixin('square', function () {
    return $this->map(function ($v) { return $v * $v; });
});

underscore([1, 2, 3])->square()->get(); // [1, 4, 9]

now(): float

The current time in millisec.

underscore()->now(); // 1529996371081

omit(array|...string|...int $index): self

Omit the items having one of the blacklisted indexes.

$u = underscore(['a' => 3, 7, 'b' => 'B', 1 => ['c', 5]]);

$u->omit('a', 0)->get(); // ['b' => 'B', 1 => ['c', 5]]

pairs(): self

Pair all items to use an array of index and value.

$u = ['a' => 3, 7, 'b' => 'B'];

$u->pair(); // ['a' => ['a', 3], 0 => [0, 7], 'b' => ['b', 'B']

pick(array|...string|...int $index): self

Pick only the items having one of the whitelisted indexes.

$u = underscore(['a' => 3, 7, 'b' => 'B', 1 => ['c', 5]]);

$u->pick(0, 1)->get(); // [7, 1 => ['c', 5]]

tap(callable $fn): self

Invokes callback fn with clone and returns original self.

$u = underscore([1, 2]);

$tap = $u->tap(function ($_) { return $_->values(); });

$tap === $u; // true

toArray(): array

Convert the data items to array.

$u = underscore([1, 3, 5, 7]);

$u->toArray(); // [1, 3, 5, 7]

valueOf(): string

Get string value (JSON representation) of this instance.

underscore(['a', 2, 'c' => 3])->valueOf(); // {0: "a", 1: 2, "c":3}

values(): self

Get all the values.

$u = underscore(['a' => 1, 'b' => 2, 'c' => 3, 5]);

$u->values()->get(); // [1, 2, 3, 5]

UnderscoreAliases

collect(callable $fn): self

Alias of map().

detect(callable $fn, bool $useValue): mixed|null

Alias of find().

drop(int $n): array|mixed

Alias of last().

foldl(callable $fn, mixed $memo): mixed

Alias of reduce().

foldr(callable $fn, mixed $memo): mixed

Alias of reduceRight().

head(int $n): array|mixed

Alias of first().

includes(): void

Alias of contains().

inject(callable $fn, mixed $memo): mixed

Alias of reduce().

select(callable|string|null $fn): self

Alias of filter().

size(): int

Alias of count().

tail(int $n): array|mixed

Alias of last().

take(int $n): array|mixed

Alias of first().

uniq(callable|string $fn): self

Alias of unique().

without(array|mixed $data): self

Alias of difference().


HigherOrderMessage

A syntatic sugar to use elegant shorthand oneliner for complex logic often wrapped in closures. See example below:

// Higher Order Messaging
class HOM
{
    protected $n;
    public $square;

    public function __construct($n)
    {
        $this->n      = $n;
        $this->square = $n * $n;
    }

    public function even()
    {
        return $this->n % 2 === 0;
    }
}

$u = [new HOM(1), new HOM(2), new HOM(3), new HOM(4)];

// Filter `even()` items
$evens = $u->filter->even(); // 'even()' method of each items!

// Map each evens to their squares
$squares = $evens->map->square; // 'square' prop of each items!
// Gives an Underscore instance

// Get the data
$squares->get();
// [1 => 4, 3 => 16]

Without higher order messaging that would look like:

$evens = $u->filter(function ($it) {
    return $it->even();
});

$squares = $evens->map(function ($it) {
    return $it->square;
});

\ArrayAccess

Underscore instances can be treated as array:

$u = underscore([1, 2, 'a' => 3]);

isset($u['a']); // true
isset($u['b']); // false

echo $u[1];     // 2

$u['b'] = 'B';
isset($u['b']); // true

unset($u[1]);

Arrayizes

You can use this trait to arrayize all complex data.

use Ahc\Underscore\Arrayizes;

class Any
{
    use Arrayizes;

    public function name()
    {
        $this->asArray($data);
    }
}

License

MIT | © 2017-2018 | Jitendra Adhikari