athari/yalinqo

YaLinqo, a LINQ-to-objects library for PHP

Installs: 1 103 376

Dependents: 9

Suggesters: 0

Security: 0

Stars: 445

Watchers: 21

Forks: 40

Open Issues: 11

pkg:composer/athari/yalinqo

v2.5.2 2025-10-07 02:00 UTC

README

Travis CI Status Coveralls Coverage Scrutinizer Code Quality Packagist Downloads Packagist Version GitHub License

Features

  • The most complete port of .NET LINQ to PHP, with many additional methods.
  • Lazy evaluation, error messages and other behavior of original LINQ.
  • Detailed PHPDoc and online reference based on PHPDoc for all methods. Articles are adapted from original LINQ documentation from MSDN.
  • 100% unit test coverage.
  • Best performance among full-featured LINQ ports (YaLinqo, Ginq, Pinq), at least 2x faster than the closest competitor, see performance tests.
  • Callback functions can be specified as arrow functions (fn($v) => $v), first-class callables (strnatcmp(...)) or any other PHP callables.
  • Keys are as important as values. Most callback functions receive both values and keys; transformations can be applied to both values and keys; keys are never lost during transformations, if possible.
  • SPL interfaces Iterator, IteratorAggregate etc. are used throughout the code and can be used interchangeably with Enumerable.
  • Redundant collection classes are avoided, native PHP arrays are used everywhere.
  • Composer support (package on Packagist).
  • No external dependencies.

Implemented methods

Some methods had to be renamed, because their names are reserved keywords. Original methods names are given in parenthesis.

  • Generation: cycle, emptyEnum (empty), from, generate, toInfinity, toNegativeInfinity, matches, returnEnum (return), range, rangeDown, rangeTo, repeat, split;
  • Projection and filtering: cast, ofType, select, selectMany, where;
  • Ordering: orderBy, orderByDescending, orderByDir, thenBy, thenByDescending, thenByDir;
  • Joining and grouping: groupJoin, join, groupBy;
  • Aggregation: aggregate, aggregateOrDefault, average, count, max, maxBy, min, minBy, sum;
  • Set: all, any, append, concat, contains, distinct, except, intersect, prepend, union;
  • Pagination: elementAt, elementAtOrDefault, first, firstOrDefault, firstOrFallback, last, lastOrDefault, lastOrFallback, single, singleOrDefault, singleOrFallback, indexOf, lastIndexOf, findIndex, findLastIndex, skip, skipWhile, take, takeWhile;
  • Conversion: toArray, toArrayDeep, toList, toListDeep, toDictionary, toJSON, toLookup, toKeys, toValues, toObject, toString;
  • Actions: call (do), each (forEach), write, writeLine.

In total, more than 80 methods.

Usage

Add to composer.json:

{
    "require": {
        "athari/yalinqo": "^2.0"
    }
}

Add to your PHP script:

require_once 'vendor/autoloader.php';
use \YaLinqo\Enumerable;

// 'from' can be called as a static method or via a global function shortcut
Enumerable::from([1, 2, 3]);
from([1, 2, 3]);

Example

Process sample data:

// Data
$products = [
    [ 'name' => 'Keyboard',    'catId' => 'hw', 'quantity' =>  10, 'id' => 1 ],
    [ 'name' => 'Mouse',       'catId' => 'hw', 'quantity' =>  20, 'id' => 2 ],
    [ 'name' => 'Monitor',     'catId' => 'hw', 'quantity' =>   0, 'id' => 3 ],
    [ 'name' => 'Joystick',    'catId' => 'hw', 'quantity' =>  15, 'id' => 4 ],
    [ 'name' => 'CPU',         'catId' => 'hw', 'quantity' =>  15, 'id' => 5 ],
    [ 'name' => 'Motherboard', 'catId' => 'hw', 'quantity' =>  11, 'id' => 6 ],
    [ 'name' => 'Windows',     'catId' => 'os', 'quantity' => 666, 'id' => 7 ],
    [ 'name' => 'Linux',       'catId' => 'os', 'quantity' => 666, 'id' => 8 ],
    [ 'name' => 'Mac',         'catId' => 'os', 'quantity' => 666, 'id' => 9 ],
];
$categories = [
    [ 'name' => 'Hardware',          'id' => 'hw' ],
    [ 'name' => 'Operating systems', 'id' => 'os' ],
];

// Put products with non-zero quantity into matching categories;
// sort categories by name;
// sort products within categories by quantity descending, then by name.
$result = from($categories)
    ->orderBy(fn($cat) => $cat['name'])
    ->groupJoin(
        from($products)
            ->where(fn($prod) => $prod['quantity'] > 0)
            ->orderByDescending(fn($prod) => $prod['quantity'])
            ->thenBy(fn($prod) => $prod['name'], 'strnatcasecmp'),
        fn($cat) => $cat['id'],
        fn($prod) => $prod['catId'],
        fn($cat, $prods) => [
            'name' => $cat['name'],
            'products' => $prods
        ]
    );

// More verbose syntax with parameter names (PHP 8.0+)
// and first-class callables (PHP 8.1+):
$result = Enumerable::from($categories)
    ->orderBy(keySelector: fn($cat) => $cat['name'])
    ->groupJoin(
        inner: from($products)
            ->where(predicate: fn($prod) => $prod['quantity'] > 0)
            ->orderByDescending(keySelector: fn($prod) => $prod['quantity'])
            ->thenBy(keySelector: fn($prod) => $prod['name'], comparer: strnatcasecmp(...)),
        outerKeySelector: fn($cat) => $cat['id'],
        innerKeySelector: fn($prod) => $prod['catId'],
        resultSelectorValue: fn($cat, $prods) => [
            'name' => $cat['name'],
            'products' => $prods
        ]
    );

print_r($result->toArrayDeep());

Output (compacted):

Array (
    [hw] => Array (
        [name] => Hardware
        [products] => Array (
            [0] => Array ( [name] => Mouse       [catId] => hw [quantity] =>  20 [id] => 2 )
            [1] => Array ( [name] => CPU         [catId] => hw [quantity] =>  15 [id] => 5 )
            [2] => Array ( [name] => Joystick    [catId] => hw [quantity] =>  15 [id] => 4 )
            [3] => Array ( [name] => Motherboard [catId] => hw [quantity] =>  11 [id] => 6 )
            [4] => Array ( [name] => Keyboard    [catId] => hw [quantity] =>  10 [id] => 1 )
        )
    )
    [os] => Array (
        [name] => Operating systems
        [products] => Array (
            [0] => Array ( [name] => Linux       [catId] => os [quantity] => 666 [id] => 8 )
            [1] => Array ( [name] => Mac         [catId] => os [quantity] => 666 [id] => 9 )
            [2] => Array ( [name] => Windows     [catId] => os [quantity] => 666 [id] => 7 )
        )
    )
)

Versions

Version Status PHP Notes

1.x (2012)

1.0−1.1 legacy 5.3−7.4
  • Manually implemented iterators

2.x (2014)

2.0−2.4 legacy 5.5−7.4
  • Rewrite using PHP 5.5 generators
  • Causes deprecation warnings in PHP 7.2+ due to use of create_function
2.5 maintenance 5.5+
  • Switched from create_function to eval for string lambdas
  • May cause security analysis warnings due to use of eval

3.x (2018)

3.0 abandoned 7.0+
  • Abandoned rewrite with perfomance improvements
  • Released 7 years later with most of the performance-related changes dropped
  • May cause security analysis warnings due to use of eval

4.x (2025)

4.0 planned 8.0+(?)
  • Strong types everywhere, string lambdas nuked from existence

Breaking changes

Version 1.x → 2.x

  • Minimum supported PHP version is 5.5.
  • Collections Dictionary and Lookup were replaced with standard arrays.

Version 2.x → 3.x

  • Minimum supported PHP version is 7.0.
  • Type hints were added to parameters of some functions (ofType, range, rangeDown, rangeTo, toInfinity, toNegativeInfinity, matches, split). There may be edge cases if you rely on passing incorrect types of arguments.

Legacy information

Legacy features

  • (Versions 1.0−2.5) Callback functions can be specified as "string lambdas" using various syntaxes:
    • '"$k = $v"' (implicit $v and $k arguments, implicit return)
    • '$v ==> $v + 1' (like a modern arrow function, but without fn and with a longer arrow)
    • '($v, $k) ==> $v + $k' (explicit arguments, implicit return)
    • '($v, $k) ==> { return $v + $k; }' (explicit arguments, explicit return within a block)

Note

Before arrow functions were added in PHP 7.4, the choice was between the ridiculously verbose anonymous function syntax (function ($value) { return $value['key']; }) and rolling your own lambda syntax (like $v ==> $v["key"]). This is why "string lambdas" were a necessity at the time.

Caution

When using legacy versions of YaLinqo and PHP:

  1. You MUST NOT1 use user-provided strings to construct string lambdas. This directly opens you to passing to user-provided strings to eval, which is literally the worst thing you can do security-wise.
  2. You SHOULD NOT1 dynamically construct string lambdas in general, even if it seems convenient. Passing incorrect code to eval throws a ParseError. An exception to this rule may be constructing a trivial lambda from an array of predefined values.
  3. You SHOULD1 use full closure syntax instead of string lambdas when you need access to variables in scope.

When all your string lambdas are single-quoted string constants, there's no security risk in using them. If you're still paranoid about eval, just never use string lambdas.

Links

Documentation

Tip

If you're new to LINQ, you should read the series of articles by Mr. X, as they're very beginner-friednly.

Articles

Alternatives

Realistically, there're none. This is the only PHP library in existence which implements lazy evaluation, deals with keys in iterators properly, has documentation and actually works (until yet another breaking change in PHP), with everything else failing in 2+ ways. However, some alternatives are worth mentioning.

  • Laravel LazyCollection (Laravel 6.0+) — The closest you can get to LINQ-to-objects in PHP without YaLinqo. Includes SQL-isms like where('balance', '>', '100'), Ruby-isms like pluck('my.hair'), random non-pure methods like forget('name') and other footguns, but largely functional. Note that lazy evaluation is opt-in: you need to call either LazyCollection::make($iterable) or collect($array)->lazy().
  • RxPHP — reactive (push) counterpart of the active (pull) LINQ, port of Rx.NET. A faithful implementation of Rx in PHP by people who actually use it. Highly recommended if you need complex transformations over asynchronous operations.

Related projects

  • linq.js — LINQ for JavaScript. The one and only complete port of .NET LINQ to JavaScript. Supports TypeScript, ESM, CJS, browsers.
  • Underscore.js — library for functional programming in JavaScript. Similar to LINQ, but different method names and no lazy evaluation.
  • YaLinqoPerf — collection of performance tests comparing raw PHP, array functions, YaLinqo, YaLinqo with string lambdas, Ginq, Ginq with property accessors, Pinq.

PHP

If you want to contribute to the project without writing any code, consider annoying the developers of PHP on GitHub and their mailing list whenever they decline yet another useful feature.

If you're successful and actually get them to implement PFA + Pipe v4 (?), then non-lazy LINQ ports will lose 80% of their users, as PHP array functions will become usable by themselves without turning the code into unreadable spaghetti.

And if devs of PHP implement pipes for iterables, then YaLinqo itself will need a complete rewrite for 20% of cases and become obsolete for 80% of them. I wouldn't hold my breath though, as that thing has been in discussion for like 10 years already.

  • Graveyard of PHP RFCs:

  • Too little too late:

    • Pipe operator v3 (v2, v1) — took 3 RFCs and 10 years, but we've finally arrived at... the least useful and the most verbose pipe syntax on the planet... yay?
    • Arrow functions v2 (v1, v0) — took 3 RFCs and just 5 years. A notable exception of actually being in a good state. However, zero plans from the "future scope" were implemented in the following years.

License

Simplified BSD License
Copyright © 2012–2025, Alexander Prokhorov

Footnotes

  1. The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. 2 3