Search by

mathsgod / php-linq

mathsgod

A provider-based LINQ-style query library for PHP

Package info

github.com/mathsgod/php-linq

pkg:composer/mathsgod/php-linq

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-09-22 04:56 UTC

This package is auto-updated.

Last update: 2026-09-22 05:01:00 UTC


README

An experimental LINQ-style query library for PHP 8.2+. Unlike callback-only collection wrappers, a query is represented by an expression tree and executed by a query provider.

LINQ to Objects

Enumerable provides lazy, generator-based processing with ordinary PHP callbacks:

use PhpLinq\Enumerable;

$names = Enumerable::from($users)
    ->where(fn (User $user): bool => $user->active)
    ->orderBy(fn (User $user): int => $user->age)
    ->select(fn (User $user): string => $user->name)
    ->take(10)
    ->toArray();

Streaming operators include where, select, selectMany, skip, take, skipWhile, takeWhile, distinct, append, prepend, concat, and chunk. Operators which require a complete view of the sequence, such as orderBy, reverse, and takeLast, buffer their input when enumerated.

The API also includes the .NET-style aggregate and set operators min, max, minBy, maxBy, except, intersect, union, and shuffle. Relational join combines matching outer and inner elements by key:

$summaries = Enumerable::from($users)->join(
    $orders,
    fn (array $user): int => $user['id'],
    fn (array $order): int => $order['userId'],
    fn (array $user, array $order): array => [
        'name' => $user['name'],
        'total' => $order['total'],
    ],
);

Call asEnumerable() on an IQueryable to execute the provider-backed part first and continue with callback-based, in-memory operators.

Generic collections

Mutable collections modelled after System.Collections.Generic are available under PhpLinq\\Collections\\Generic:

use PhpLinq\Collections\Generic\GenericList;
use PhpLinq\Collections\Generic\Queue;
use PhpLinq\Collections\Generic\Stack;

$list = new GenericList([1, 2]);
$list->add(3);
$list->insert(0, 0);
$list->insertRange(1, [10, 20]);
$slice = $list->getRange(1, 2);
$list->removeRange(1, 2);

$stack = new Stack();
$stack->push('job');
$job = $stack->pop();

$queue = new Queue();
$queue->enqueue('job');
$job = $queue->dequeue();

PHP reserves the keyword list, so the .NET List<T> equivalent is named GenericList<T>. All three collections implement IteratorAggregate and can be passed directly to Enumerable::from().

The generic collection contracts are organised as follows:

IReadOnlyCollection<T>
├── ICollection<T>
│   ├── IList<T> → GenericList<T>
│   └── ISet<T>  → HashSet<T>
├── Stack<T>
└── Queue<T>

IReadOnlyCollection<KeyValuePair<TKey, TValue>>
└── IDictionary<TKey, TValue> → Dictionary<TKey, TValue>

HashSet<T> supports custom EqualityComparer<T> implementations and mutable set operations including union, intersection, difference, symmetric difference, subset/superset checks, overlap checks, and set equality.

LINQ to XML

The PhpLinq\\Xml namespace provides a DOMDocument-backed XML object model:

use PhpLinq\Xml\XDocument;
use PhpLinq\Xml\XElement;

$document = XDocument::load('users.xml');

$names = $document
    ->descendants('user')
    ->where(fn (XElement $user): bool =>
        $user->attribute('active')?->value() === 'true'
    )
    ->select(fn (XElement $user): ?string =>
        $user->element('name')?->value()
    )
    ->toArray();

XDocument supports parsing, file loading, root access, descendant queries, and XML serialization. XElement provides direct-child and descendant element queries plus attribute access. Expanded names such as {urn:people}user are supported through XName. All XML sequence methods return lazy IEnumerable instances.

use PhpLinq\Expr;
use PhpLinq\InMemoryQueryProvider;
use PhpLinq\Queryable;

$provider = new InMemoryQueryProvider([
    'users' => [
        ['name' => 'Ada', 'active' => true, 'age' => 36],
        ['name' => 'Bob', 'active' => false, 'age' => 22],
    ],
]);

$names = Queryable::from($provider, 'users')
    ->where(Expr::eq(Expr::field('active'), true))
    ->orderBy(Expr::field('name'))
    ->select(Expr::field('name'))
    ->toArray();

Select multiple fields with a projection. fields() keeps the field names, while projection() allows aliases and computed expressions:

$summaries = $users->select(Expr::fields('id', 'name'))->toArray();

$summaries = $users->select(Expr::projection([
    'userId' => Expr::field('id'),
    'displayName' => Expr::field('name'),
    'isAdult' => Expr::gte(Expr::field('age'), 18),
]))->toArray();

select(Expr::field('name')) returns a scalar sequence. fields() and projection() always return associative rows keyed by their aliases, even when they contain only one member.

The expression tree is provider-independent. InMemoryQueryProvider executes it against arrays or other iterables; a future SQL provider can translate the same nodes into parameterized SQL.

Supported query operators: where, select, orderBy, orderByDescending, skip, take, count, any, and first.

Generic dictionary

Collections\\Generic\\Dictionary<TKey, TValue> is a hash-table collection with object-key support and pluggable equality semantics:

use PhpLinq\Collections\Generic\Dictionary;

/** @var Dictionary<string, int> $scores */
$scores = new Dictionary();
$scores->add('Ada', 10);       // throws when the key already exists
$scores['Ada'] = 15;           // indexer-style insert or replace
$scores->tryGetValue('Ada', $score);

It provides add, set, get, tryAdd, tryGetValue, containsKey, containsValue, remove, clear, keys, and values. Iteration returns KeyValuePair<TKey, TValue> objects so keys are not restricted to PHP's integer and string iterator-key types.

SQL providers

The same expression tree can be compiled and executed through PDO:

use PhpLinq\Queryable;
use PhpLinq\Sql\MySqlDialect;
use PhpLinq\SqlQueryProvider;

$users = Queryable::from(
    new SqlQueryProvider($pdo, new MySqlDialect()),
    'users',
);

The included dialects are MySqlDialect, SqlServerDialect, PostgreSqlDialect, and SqliteDialect. They generate their native identifier quoting and pagination syntax, including SQL Server TOP / OFFSET ... FETCH and MySQL, PostgreSQL, and SQLite LIMIT variants. Query values are always emitted as bound parameters.

Install the development dependencies and run the PHPUnit test suite with:

composer install
composer test