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
Requires
- php: >=5.5
Requires (Dev)
README
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 withEnumerable
. - 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
Breaking changes
Version 1.x → 2.x
- Minimum supported PHP version is 5.5.
- Collections
Dictionary
andLookup
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 withoutfn
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:
- 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. - You SHOULD NOT1 dynamically construct string lambdas in general, even if it seems convenient. Passing incorrect code to
eval
throws aParseError
. An exception to this rule may be constructing a trivial lambda from an array of predefined values. - 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
- Reference documentation — generated from source. Includes MSDN-tier explanation of what every method does.
- How to use Linq in PHP (acrhived) by Mr. X — a series of posts covering basic usage of YaLinqo.
Tip
If you're new to LINQ, you should read the series of articles by Mr. X, as they're very beginner-friednly.
Articles
-
CodeProject (English):
- LINQ for PHP comparison: YaLinqo, Ginq, Pinq — performance comparison of full-featured LINQ ports, with some additional information.
-
Habrahabr (Russian):
- Comparison of old LINQ libraries — comparison of LINQ for PHP, Phinq, PHPLinq and Plinq, also Underscore.php.
- YaLinqo 1.0 with updated comparison — explanation of architecture and design decisions.
- YaLinqo 2.0 — switch to PHP 5.5 with generators support and related changes.
- LINQ for PHP: speed matters — performance comparison of full-featured LINQ ports (YaLinqo, Ginq, Pinq).
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 likepluck('my.hair')
, random non-pure methods likeforget('name')
and other footguns, but largely functional. Note that lazy evaluation is opt-in: you need to call eitherLazyCollection::make($iterable)
orcollect($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:
- Partial function application — imagine pipe operator being actually useful... nah, not happening.
- Short Closures 2.0 — support for multi-statement arrow functions declined, 2 votes short.
- Add
array_group
function — grouping won't be optimized by using a built-in function. Not a huge loss, but still a pity.
-
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