YaLinqo
Yet Another LINQ to Objects for PHP [Simplified BSD]
Install / Use
/learn @Athari/YaLinqoREADME
YaLinqo: Yet Another LINQ to Objects for PHP
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,IteratorAggregateetc. 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 parentheses.
- 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": "^3.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
<!--suppress HtmlDeprecatedAttribute --> <table> <tr> <th>Version</th> <th>Status</th> <th>PHP</th> <th>Notes</th> <tr> <td colspan=4><h3><b>1.x</b> (2012) <tr> <td valign=top>1.0−1.1 <td valign=top>legacy <td valign=top>5.3−7.4 <td><ul> <li>Manually implemented iterators <tr> <td colspan=4><h3><b>2.x</b> (2014) <tr> <td valign=top>2.0−2.4 <td valign=top>legacy <td valign=top>5.5−7.4 <td><ul> <li>Rewrite using PHP 5.5 generators <li>Causes deprecation warnings in PHP 7.2+ due to use of <code>create_function</code> <tr> <td valign=top>2.5 <td valign=top>maintenance <td valign=top>5.5+ <td><ul> <li>Switched from <code>create_function</code> to <code>eval</code> for string lambdas <li>May cause security analysis warnings due to use of <code>eval</code> <tr> <td colspan=4><h3><b>3.x</b> (2018) <tr> <td valign=top>3.0 <td valign=top>maintenance <td valign=top>7.0+ <td><ul> <li>Abandoned rewrite with performance improvements <li>Released 7 years later with most of the performance-related changes dropped <li>May cause security analysis warnings due to use of <code>eval</code> <tr> <td colspan=4><h3><b>4.x</b> (2025) <tr> <td valign=top>4.0 <td valign=top>planned <td valign=top>8.0+(?) <td><ul> <li>Strong types everywhere, string lambdas nuked from existence </table>Breaking changes
Version 1.x → 2.x
- Minimum supported PHP version is 5.5.
- Collections
DictionaryandLookupwere 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$vand$karguments, implicit return)'$v ==> $v + 1'(like a modern arrow function, but withoutfnand 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 NOT[^1] 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 NOT[^1] dynamically construct string lambdas in general, even if it seems convenient.
