Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Qarray Laravel Package

nahid/qarray

QArray is a lightweight PHP package for querying arrays with a fluent, SQL-like API. Filter, sort, group, and pluck structured data using expressive methods, making it useful in Laravel apps and any PHP project that needs simple in-memory array queries.

View on GitHub
Deep Wiki
Context7

Getting Started

nahid/qarray is a framework-agnostic query builder that runs SQL-ish, fluent queries over plain PHP arrays. It is the tool you reach for when your "database" is a JSON file, a cached API response, a config tree, or an in-memory fixture — and you want where/orderBy/limit ergonomics instead of hand-rolled array_filter loops.

Install and verify PHP compatibility first

composer require nahid/qarray
php -r "require 'vendor/autoload.php'; var_dump(class_exists('Nahid\QArray\QueryEngine'));"

There is no service provider and no Laravel dependency, so it drops into plain PHP, Laravel, Symfony, or a CLI script equally. Confirm the installed tag (composer show nahid/qarray) — the package is small and the API surface has shifted across majors. Treat the README for your pinned version as the source of truth, not blog posts.

Where to look first

  1. The QueryEngine class — it is the whole package. Read its operator list and its from()/where() signatures before anything else.
  2. The tests directory in vendor/nahid/qarray — the fastest way to learn the real semantics of like, nested key resolution, and aggregates, because the README is terse.
  3. The license (CC0-1.0) — public domain, meaning no warranty and no support obligation. Budget accordingly.

First use case: querying a cached payload

The canonical day-one win is replacing nested foreach + array_filter against an API response:

use Nahid\QArray\QueryEngine;

$payload = json_decode(file_get_contents('storage/customers.json'), true);

$q = new QueryEngine($payload);

$activeUk = $q->from('customers')
    ->where('status', 'active')
    ->where('country', 'GB')
    ->orderBy('created_at', 'desc')
    ->limit(25)
    ->get();

Read this as: "pick the customers root key, filter, sort, cap, materialise." from() selects the subtree, the chain builds the predicate list, and get() is the terminal that actually executes. Nothing happens until a terminal (get, first, count, …) is called.

Start by writing one real query against one real fixture and dumping the result. Once you trust the output shape, wrap it — see the next section.

Implementation Patterns

Wrap it behind a small repository class. This is the single most important pattern. The package is at 106 stars, one maintainer, and a low adoption score; you do not want QueryEngine calls scattered across twenty controllers.

final class CustomerRepository
{
    public function __construct(private array $data) {}

    public function search(array $filters): array
    {
        $q = new QueryEngine($this->data);
        $query = $q->from('customers');

        if ($filters['status'] ?? null) {
            $query->where('status', $filters['status']);
        }
        if ($filters['country'] ?? null) {
            $query->where('country', $filters['country']);
        }

        return $query->orderBy('created_at', 'desc')->get();
    }
}

Everything downstream depends on your interface, not on QueryEngine. If the package stalls, you swap the internals for collect()->where() in an afternoon.

Build queries conditionally, not statically. The fluent chain is mutable, so you can assemble it from request input. Always allowlist field names against a known schema before passing them into where()/orderBy() — the builder does not sanitise anything, and an attacker-supplied orderBy key will simply produce empty results or a warning rather than an error you can catch.

$sortable = ['created_at', 'name', 'total_spend'];
$field = in_array($req['sort'] ?? '', $sortable, true) ? $req['sort'] : 'created_at';

Prefer it over array_filter when the query comes from data. If the predicate is hardcoded, array_filter is clearer and faster. qarray earns its keep when the predicate is dynamic — a filter panel, a saved-search feature, an admin table, a reporting export — because you get like, comparison operators, ordering, and aggregates without writing a mini-DSL yourself.

Use aggregates as terminals for dashboards and totals.

$q->from('orders')->where('region', 'emea')->count();
$q->from('orders')->where('status', 'paid')->sum('amount');

This is where the package saves the most lines: computing a filtered count/sum/avg over a nested array manually is tedious and error-prone.

Use first() for existence checks and single-record lookups rather than get()[0] ?? null; it short-circuits and keeps intent obvious.

Laravel integration. Do not register it as a singleton — each query needs its own instance, because the builder accumulates state. Construct it per-request inside a service or action class, and prefer injecting your repository above it. For Blade/admin tables, keep the terminal call in the controller and pass a plain array to the view.

Mutation: verify before you rely on it. If your version exposes insert/update/delete, remember PHP arrays are copy-on-write value types. Any write either mutates the engine's internal copy (and needs a save()-style call or a get() that returns the whole tree) or is lost. Write a two-line test asserting the caller's original array changed before you build a feature on it — and if it does not persist, keep writes out of this package entirely and mutate the array yourself.

Testing pattern that pays off immediately. For each repository method, assert the qarray result equals the equivalent collect($data)->where(...) result on a fixture. This catches operator-semantics surprises early and gives you a ready-made oracle the day you want to drop the dependency.

Gotchas and Tips

No indexes, no plans, O(n) per predicate. Every where walks the full array, and each terminal re-executes the whole chain. On a 500-row config dump this is irrelevant; on a 200k-row export it will be slower and more memory-hungry than a single array_filter, because of the intermediate copies. Benchmark at your real data size before committing, and keep qarray for small-to-medium datasets — that is the honest scope of the tool.

PHP's loose comparison will bite you. where('id', 10) against a JSON-decoded "10" string behaves differently depending on whether the builder uses == or ===. JSON from APIs gives you strings where you expect ints, and "0"/0/null/"" are a classic trap. Normalise types at the boundary (cast on ingest) rather than debugging mysterious empty result sets.

like is not SQL LIKE. Expect %-wildcard substring matching implemented with strpos/regex under the hood — typically case-sensitive, with no collation, no _ single-char wildcard, and no escaping of literal %. Test it against mixed-case data before shipping a search box.

Nested keys are resolved by dot notation and are ambiguous. where('user.address.city', 'Dhaka') is convenient until your data genuinely contains a key named "user.address". Be explicit about which convention your fixture uses, and flatten awkward trees at ingest time.

Comparisons are PHP-native. Dates compare as strings — fine for ISO-8601 (YYYY-MM-DD sorts lexicographically), broken for d/m/Y. Booleans, null, and numeric strings all need a deliberate convention.

Errors are quiet. Missing from() keys, unknown operators, and unresolvable paths tend to yield empty arrays rather than exceptions. When a query returns nothing, debug in this order: (1) get() the unfiltered from() subtree — does it even have data? (2) add predicates one at a time; (3) dump the value type at the failing predicate. A silent empty result is almost always a path or type mismatch, not a bug in the package.

Watch for state leaking between queries. A QueryEngine instance accumulates where clauses. Reusing one instance across two logical queries (e.g. a loop, or a container singleton) silently compounds filters. New instance per query, always.

Extension points are thin. Custom operators, if supported, are registered on the engine — check whether that is a static map or per-instance. If you need more than two custom operators, you have outgrown the package; subclassing it is usually worse than writing a 60-line filter class you fully control.

Maintenance risk is the real gotcha. Low stars, a single maintainer, a terse README, a public-domain licence with no warranty, and a low opportunity score mean: pin the exact version in composer.json, keep the dependency behind one interface, keep the equivalence tests, and do not let it creep into your domain model. Used as a thin, swappable query helper over small arrays, it is a genuinely pleasant time-saver. Used as an ORM substitute, it will hurt.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin