heliomarpm / linq-php
Manipulação fluida de arrays em PHP inspirada no LINQ: filtros, joins relacionais, agrupamentos e projeções com alta performance e zero dependências.
Fund package maintenance!
Requires
- php: >=8.1
Requires (Dev)
- heliomarpm/db-php: ^1.0
- laravel/pint: ^1.20 || ^1.32
- phpbench/phpbench: ^1.7
- phpstan/phpstan: *
- phpunit/phpunit: ^11.0 || ^12.0 || ^13.4
- rector/rector: ^2.7
Suggests
- heliomarpm/db-php: Driver de banco de dados leve (PDO/MySQLi) com integração nativa para coleções LINQ
Provides
None
Conflicts
None
Replaces
None
README
LinqPHP é uma biblioteca leve, de alta performance e sem dependências externas para manipulação de coleções de dados (arrays) em PHP, inspirada na sintaxe do LINQ (Language Integrated Query).
Permite filtrar, agrupar, ordenar e unir arrays multidimensionais de forma fluida e elegante, sendo ideal para processamento de relatórios, formatação de dados para APIs e refatoração de sistemas legados.
Benchmark
Requisitos
- PHP 8.1 ou superior.
Instalação
Você pode instalar o pacote via Composer:
composer require heliomarpm/linq-php
Como usar
A classe LinqPHP utiliza uma interface fluida (method chaining). Todas as operações retornam a própria instância (exceto os métodos de saída toArray e toObject), permitindo encadear múltiplas transformações.
Inicialização
require 'vendor/autoload.php'; use HeliomarPM\LinqPHP\LinqPHP; $data = [ ['id' => 1, 'name' => 'John', 'age' => 25, 'status' => 'active', 'city' => 'London'], ['id' => 2, 'name' => 'Jane', 'age' => 30, 'status' => 'pending', 'city' => 'New York'], ['id' => 3, 'name' => 'John', 'age' => 35, 'status' => 'inactive', 'city' => 'Paris'], ]; $linq = LinqPHP::from($data);
💡 Dica (Uso Opcional): Consultando dados do Banco de Dados com DbPHP
Para extrair dados do MySQL, SQLite ou PostgreSQL com alta performance, zero dependências e telemetria nativa, utilize a biblioteca parceira heliomarpm/db-php (Demo interativa):
composer require heliomarpm/db-php
O método ->linq() do DbPHP integra-se perfeitamente ao LinqPHP, permitindo encadear filtros e transformações em memória direto da query SQL:
use HeliomarPM\DbPHP\DB; // Consulta o banco e continua o pipeline diretamente no LinqPHP via ->linq(): $ativos = DB::query("SELECT id, nome, limite_credito, status FROM clientes WHERE status = :status", ["status" => "ativo"]) ->linq() ->where(fn($c) => $c['limite_credito'] > 1000) ->orderByKey('nome') ->toArray();
where() - Filtragem de Dados
Aceita condições estruturadas no formato ['campo', 'operador', $valor], arrays de múltiplas condições ou funções anônimas (closures) para filtros avançados.
Operadores Suportados
| Categoria | Operador | Exemplo | Descrição |
|---|---|---|---|
| Igualdade | =, == |
['status', '=', 'active'] |
Igualdade ampla (==) |
=== |
['codigo', '===', 0] |
Igualdade estrita de tipo e valor (===) |
|
| Desigualdade | !=, <> |
['status', '!=', 'inactive'] |
Desigualdade ampla (!= ou <>) |
!== |
['codigo', '!==', '0'] |
Desigualdade estrita de tipo e valor (!==) |
|
| Relacionais | >, >= |
['age', '>=', 18] |
Maior que, maior ou igual a |
<, <= |
['price', '<=', 100.0] |
Menor que, menor ou igual a | |
| Intervalos | between |
['price', 'between', [50.0, 150.0]] |
Intervalo fechado inclusivo (min <= x && x <= max) |
not between |
['price', 'not between', [10.0, 50.0]] |
Fora do intervalo especificado (`x < min | |
| Listas / Sets | in |
['role', 'in', ['admin', 'manager']] |
Pertence à lista (otimizado com hashmap O(1)) |
not in |
['role', 'not in', ['banned', 'guest']] |
Não pertence à lista (hashmap O(1) negado) | |
| Texto / Strings | startswith |
['sku', 'startswith', 'PROD-'] |
Inicia com o prefixo informado |
endswith |
['email', 'endswith', '@empresa.com'] |
Termina com o sufixo informado | |
contains |
['title', 'contains', 'relatório'] |
Contém a substring informada |
Tip
Performance O(1) em in e not in:
Listas passadas para in e not in são indexadas em hashmap via array_flip durante a normalização da query. Em vez de varreduras lineares $O(M)$ do in_array, a checagem é feita em tempo constante $O(1)$ via isset() com preservação estrita de tipo.
// 1. Filtro simples por igualdade ou comparador numérico $result = LinqPHP::from($data) ->where(['age', '>', 25]) ->toArray(); // 2. Intervalo numérico (between / not between) e listas (in / not in) $result = LinqPHP::from($data) ->where([ ['price', 'between', [50.0, 200.0]], ['status', 'not in', ['cancelado', 'devolvido']], ['role', 'in', ['admin', 'supervisor']] ]) ->toArray(); // 3. Desigualdade e busca textual $result = LinqPHP::from($data) ->where([ ['status', '!=', 'inativo'], ['name', 'startswith', 'J'] ]) ->toArray(); // 4. Comparação sensível a maiúsculas/minúsculas (caseSensitive: true) $result = LinqPHP::from($data) ->where(['sku', 'startswith', 'PROD-A'], caseSensitive: true) ->toArray(); // 5. Usando Closures customizadas para regras dinâmicas $result = LinqPHP::from($data) ->where(fn($item) => $item['id'] % 2 !== 0 && !empty($item['email'])) ->toArray();
select() e distinct() - Projeção de Dados
Selecione apenas as colunas desejadas, renomeie campos utilizando aliases ou misture ambos com a projeção híbrida.
// 1. Seleção simples de colunas $result = LinqPHP::from($data) ->select(['name', 'status']) ->toArray(); // 2. Seleção com Aliases (renomeação de colunas) $result = LinqPHP::from($data) ->select([ 'id' => 'codigo', 'name' => 'nome_completo', 'city' => 'cidade' ]) ->toArray(); // 3. Projeção Híbrida (mantém alguns campos originais e renomeia outros) $result = LinqPHP::from($data) ->select([ 'id', // mantém como 'id' 'name' => 'nome_completo', // renomeia para 'nome_completo' 'status' // mantém como 'status' ]) ->toArray(); // 4. Retorna registros únicos por coluna (O(1), sem serialize) $result = LinqPHP::from($data) ->distinct('name') ->toArray(); // 5. Retorna registros únicos por closure projetora $result = LinqPHP::from($data) ->distinct(fn($item) => $item['category'] . '|' . $item['status']) ->toArray(); // 6. Deduplicação integral do registro (legado — usa serialize()) $result = LinqPHP::from($data) ->select(['name']) ->distinct() ->toArray();
Tip
Performance e Modo Estrito (strict):
-
Zero Performance Penalty: Em seleções tradicionais indexadas numericamente (
['name', 'status']), o LinqPHP utiliza internamentearray_is_list()nativo do PHP 8.1+ e executa o caminho direto em C comarray_intersect_key, garantindo zero degradação de performance. -
Preservação de Ordem: As colunas no array de saída seguem rigorosamente a ordem em que foram declaradas no
select(). -
Modo Estrito: Por padrão (
strict: false), campos inexistentes na fonte são preenchidos comnull. Ao usarselect(['campo1', 'campo2'], strict: true), umaInvalidArgumentExceptionserá lançada caso alguma chave não exista no registro. -
distinct(string)é O(1): Ao passar uma coluna escalar, a chave é computada via(string)$item[$column], eliminandoserialize()e reduzindo uso de memória em até 80%.
join() - União Relacional
Faça uniões relacionais entre arrays multidimensionais como se estivesse em um banco de dados relacional. Suporta os 4 tipos de join via métodos dedicados e expressivos (innerJoin, leftJoin, rightJoin, fullJoin) ou pelo método genérico join() com o enum nativo JoinType:
innerJoin()ouJoinType::INNER: Retorna apenas os registros com correspondência em ambas as coleções.leftJoin()ouJoinType::LEFT: Retorna todos os registros da esquerda e os correspondentes da direita (ounull).rightJoin()ouJoinType::RIGHT: Retorna todos os registros da direita e os correspondentes da esquerda (ounull).fullJoin()ouJoinType::FULL: Retorna todos os registros de ambos os lados, preenchendo lacunas comnull.
$users = [ ['id' => 1, 'name' => 'John', 'course_id' => 10], ['id' => 2, 'name' => 'Jane', 'course_id' => 20], ]; $courses = [ ['id_course' => 10, 'course_name' => 'PHP 8'], ['id_course' => 20, 'course_name' => 'Clean Code'], ]; // Métodos diretos e fluidos (dispensam a importação de JoinType) $result = LinqPHP::from($users) ->innerJoin($courses, ['course_id' => 'id_course']) ->toArray(); $result = LinqPHP::from($users) ->leftJoin($courses, ['course_id' => 'id_course']) ->toArray(); // Joins Múltiplos encadeados (Join Triplo) $result = LinqPHP::from($vendas) ->innerJoin($produtos, ['produto_id' => 'id_produto']) ->leftJoin($clientes, ['cliente_id' => 'id_cliente']) ->toArray(); // Método genérico com o enum JoinType (ideal para tipos de join dinâmicos) use HeliomarPM\LinqPHP\JoinType; $result = LinqPHP::from($users) ->join($courses, JoinType::INNER, ['course_id' => 'id_course']) ->toArray();
unionAll() - Concatenação de Coleções
Consolida múltiplos arrays em uma única coleção, preenchendo automaticamente com null colunas ausentes em registros heterogêneos.
$produtosRegulares = [ ['id' => 1, 'nome' => 'Filamento PLA', 'preco' => 90.0] ]; $produtosOutlet = [ ['id' => 2, 'nome' => 'Resina 3D', 'desconto' => 0.2] // coluna 'preco' ausente ]; $result = LinqPHP::from($produtosRegulares) ->unionAll($produtosOutlet) ->toArray();
groupBy() - Agrupamento e Agregação
Agrupe dados e realize cálculos automáticos (sum, max, min, count, avg).
$vendas = [ ['vendedor' => 'João', 'valor' => 100], ['vendedor' => 'Maria', 'valor' => 200], ['vendedor' => 'João', 'valor' => 150], ]; // Agrupa por vendedor e soma os valores $result = LinqPHP::from($vendas) ->groupBy(['vendedor'], ['sum' => ['valor']]) ->toArray();
orderBy() e orderByKey() - Ordenação
Ordene por uma chave simples ou função (orderByKey), ou por múltiplos critérios com direções independentes (orderBy). Suporta nomes de campos, closures de valores calculados (fn($x) => ...) e funções comparadoras customizadas estilo usort (fn($a, $b) => ...).
// 1. Ordenação simples por chave usando booleano ($descending: true) $result = LinqPHP::from($data) ->orderByKey('age', true) // ou orderByKey('age', descending: true) ->toArray(); // 2. Ordenação simples por chave com string (retrocompatível) $result = LinqPHP::from($data) ->orderByKey('age', 'desc') ->toArray(); // 3. Ordenação por valor calculado via Closure (expressão customizada) $result = LinqPHP::from($produtos) ->orderByKey(fn($p) => $p['preco'] * $p['quantidade'], descending: true) ->toArray(); // 4. Ordenação por comparador customizado estilo usort (2 parâmetros) $result = LinqPHP::from($data) ->orderByKey(fn($a, $b) => strcmp($a['name'], $b['name'])) ->toArray(); // 5. Ordenação multi-critério com strings ('asc' / 'desc') $result = LinqPHP::from($data) ->orderBy(['name' => 'asc', 'age' => 'desc']) ->toArray(); // 6. Ordenação multi-critério com booleanos expressivos (false = ASC, true = DESC) $result = LinqPHP::from($data) ->orderBy(['name' => false, 'age' => true]) ->toArray(); // 7. Lista simples de campos (todos ASC por padrão) $result = LinqPHP::from($data) ->orderBy(['name', 'age']) ->toArray(); // 8. Multi-critério combinando campos físicos e closures calculadas $result = LinqPHP::from($vendas) ->orderBy([ 'categoria' => false, // ASC pelo campo 'categoria' [fn($v) => $v['valor'] * $v['desconto'], 'desc'], // DESC pelo valor do desconto calculado ]) ->toArray();
Important
Ciclo de Vida em Memória e Encadeamento com select() e Aliases:
O LinqPHP opera por mutação in-memory a cada chamada da cadeia fluente. Quando você utiliza o select() renomeando colunas para aliases, os nomes originais são substituídos nos dados internos:
-
Boa Prática — Ordenar antes da projeção: Ordene seus dados pela chave física de origem antes de renomeá-los para a saída:
$result = LinqPHP::from($vendas) ->where(['status', '=', 'concluido']) ->orderByKey('id_venda', 'asc') // ✅ Ordena pela chave original ->select(['id_venda' => 'Código Venda', 'valor' => 'Total']) ->toArray();
-
Ordenando após a projeção: Se chamar o
orderByapós oselect(), você deve referenciar o alias gerado, pois o nome antigo não existe mais no registro:$result = LinqPHP::from($vendas) ->select(['id_venda' => 'Código Venda', 'valor' => 'Total']) ->orderByKey('Código Venda', 'asc') // ✅ Ordena pelo alias criado no select ->toArray();
Caso tente ordenar por uma chave inexistente (como tentar usar 'id_venda' após ele ter sido transformado em 'Código Venda'), o LinqPHP disparará uma InvalidArgumentException("This field does not exist for sorting.").
skip() e take() - Paginação
Combine skip() com take() para ignorar os itens anteriores e limitar a página atual. O offset negativo é inválido e lança InvalidArgumentException.
$pagina = 2; $itensPorPagina = 10; $resultado = LinqPHP::from($vendas) ->where(['status', '=', 'concluido']) ->orderByKey('id_venda') ->skip(($pagina - 1) * $itensPorPagina) ->take($itensPorPagina) ->toArray();
skip() reindexa os itens restantes e pode ser usado sozinho; quando o offset ultrapassa o tamanho da coleção, retorna uma lista vazia.
Extração de Resultados (toArray, toObject, first, firstOrDefault, last, lastOrDefault, any, all e Agregações Escalares)
No final da cadeia fluente, invoque um método de saída terminal para extrair os dados processados ou calcular métricas pontuais com ultra-performance:
$linq = LinqPHP::from($data)->where(['age', '>', 20]); // 1. toArray() - Retorna o array tradicional processado $array = $linq->toArray(); // 2. toObject() - Retorna um stdClass com telemetria embutida (tempo e memória) $obj = $linq->toObject(); echo $obj->count; // Quantidade de registros resultantes echo $obj->elapsedTime; // Tempo total gasto na operação em segundos echo $obj->memoryUsed; // Consumo de memória formatado (ex: 18.00 MB) print_r($obj->data); // Coleção de dados processados // 3. firstOrDefault() - Retorna o primeiro elemento correspondente (com early-return) ou valor padrão $primeiro = LinqPHP::from($data)->firstOrDefault(); // primeiro item ou null se vazio // Com predicado (avalia e interrompe no primeiro match sem iterar o array inteiro): $usuario = LinqPHP::from($data)->firstOrDefault(fn($item) => $item['id'] === 10); // Com valor padrão customizado quando não encontrar: $usuario = LinqPHP::from($data)->firstOrDefault( fn($item) => $item['id'] === 999, default: ['id' => 0, 'name' => 'Visitante'] ); // first() retorna o primeiro item ou lança UnderflowException se não houver correspondência: $usuario = LinqPHP::from($data)->first(fn($item) => $item['id'] === 10); // last() retorna o último item correspondente ou lança UnderflowException se não encontrar: $ultimoUsuario = LinqPHP::from($data)->last(fn($item) => $item['status'] === 'active'); // lastOrDefault() oferece um retorno alternativo para coleções vazias ou sem correspondência: $ultimoUsuario = LinqPHP::from($data)->lastOrDefault( fn($item) => $item['status'] === 'active', default: ['id' => 0, 'name' => 'Visitante'] ); // any() interrompe ao encontrar o primeiro item correspondente: $haUsuarioAtivo = LinqPHP::from($data)->any(fn($item) => $item['status'] === 'active'); // all() interrompe ao encontrar o primeiro item que não corresponde; em coleção vazia, retorna true: $todosAtivos = LinqPHP::from($data)->all(fn($item) => $item['status'] === 'active'); // pluck() retorna os valores de uma coluna em uma lista: $nomes = LinqPHP::from($data)->pluck('name'); // Informe uma segunda coluna para indexar os valores: $nomesPorId = LinqPHP::from($data)->pluck('name', 'id'); // 4. Agregações Escalares Rápidas (sum, avg, min, max, count) // Extração direta de métricas sem a necessidade de passar pela máquina de agrupamento: $totalVendas = LinqPHP::from($vendas)->count(); $vendasPagas = LinqPHP::from($vendas)->count(fn($v) => $v['status'] === 'concluido'); $totalItens = LinqPHP::from($vendas)->where(['status', '=', 'concluido'])->sum('qtd'); $ticketMedio = LinqPHP::from($produtos)->avg('preco'); $menorPreco = LinqPHP::from($produtos)->min('preco'); $maiorEstoque = LinqPHP::from($produtos)->max('estoque'); // Suportam também closures dinâmicas calculadas: $faturamento = LinqPHP::from($itens)->sum(fn($i) => $i['qtd'] * $i['preco']);
🧪 Laboratório Interativo (Demo)
O projeto inclui uma interface web completa para experimentação e simulação de queries LINQ em tempo real, com dados de e-commerce e editor interativo de código:
composer start
# Acesse http://localhost:8000 no seu navegador
Na demo interativa você pode simular cenários avançados como Join Triplo, Union All, agregações com GroupBy e testar expressões customizadas com telemetria instantânea de memória e tempo de execução.
🛠️ Desenvolvimento e Testes
O código-fonte principal encontra-se na pasta src/. O projeto utiliza PHPStan, Laravel Pint e PHPUnit para análise estática, formatação e testes unitários.
Comandos disponíveis via Composer:
# Executa a suíte de testes unitários composer test # Executa testes com saída detalhada composer test:v # Executa benchmarks de performance do projeto (até 100.000 registros) composer bench # Executa benchmarks formais com PHPBench e relatório agregado composer perform # Executa análise estática de tipos (PHPStan nível 8) composer lint # Valida conformidade com PSR-12 (Laravel Pint) composer format # Corrige automaticamente estilo de código composer format:fix # Executa testes e benchmarks do projeto composer qa
O script composer bench desativa o Xdebug para reduzir a interferência na medição. composer perform não altera essa configuração; se o Xdebug estiver ativo, desative-o no ambiente antes de executar o PHPBench:
$env:XDEBUG_MODE = 'off' composer perform
Em shells compatíveis com Bash, use:
XDEBUG_MODE=off composer perform
🤝 Contribuições e suporte
Existem várias formas de contribuir:
- ⭐ Marcar o repositório com uma estrela
- 🐞 Relatar bugs
- 💡 Sugerir funcionalidades ou novas stacks
- 🧾 Melhorar a documentação
- 📢 Compartilhar o projeto
Para propor uma melhoria ou contribuição, veja o Guia de Contribuição.
Se precisar de ajuda ou encontrou um problema, consulte o Suporte ou abra uma Issue. Leia também o Código de Conduta.
Obrigado a todos que já contribuíram:
Criado com contrib.nn
💵 Apoie o Projeto
Se você gosta do projeto, considere fazer uma doação ao desenvolvedor via GitHub Sponsors, Ko-fi, PayPal ou Liberapay — a escolha é sua. 😉
📝 Licença
Distribuído sob a licença MIT © Heliomar P. Marques.
Sinta-se livre para utilizar, customizar e estender estas habilidades nos seus ambientes de desenvolvimento. 🔝

