This package is a structured query/filter engine for JSON-ish “cluster” values. The core idea: store a ClusterVO (a value object tree) in a JSON column, cast it with ClusterCast, and query it either through the database (driver-aware SQL) or in memory (ClusterVOCollection). It is a very young package (0 stars, no dependents, French-only docs), so treat the README as a map, not a contract — verify method signatures in src/ before building on them.
Minimal setup:
composer require andydefer/laravel-cluster
Then, based on the documented architecture, the first places to look are:
ClusterQuery — the central parser/compiler. This is where operators, AST, and SQL generation live.ClusterService — the facade you will call from controllers/services.ClusterVO — the value object you actually persist and filter on.ClusterCast — the Eloquent cast that maps the JSON column to/from ClusterVO.ClusterVOCollection — the in-memory counterpart, useful for already-loaded models and tests.A first end-to-end use case:
use Andydefer\LaravelCluster\Casts\ClusterCast; // namespace illustrative
class Product extends Model
{
protected $casts = [
'attributes' => ClusterCast::class,
];
}
$product = Product::create([
'name' => 'Shoes',
'attributes' => ClusterVO::make([
'color' => 'red',
'price' => 120,
'tags' => ['sale', 'new'],
]),
]);
Then filter through the service/query layer:
$products = ClusterService::query()
->from(Product::class)
->where('attributes.color', '=', 'red')
->where('attributes.price', '>', 100)
->get();
The exact chain above is illustrative — the README documents the concepts (ClusterQuery, ClusterService, ClusterVO, ClusterCast, ClusterVOCollection) but not every fluent signature. Start with the smallest possible query, dump the generated SQL, and confirm it matches what you expect before expanding.
First real-world use case: an attributes/metadata JSON column where users need to filter by arbitrary keys (color, price, tags, nested arrays) without you hand-writing whereJsonContains and driver-specific JSON paths everywhere. This package is trying to be that abstraction layer.
1. Cast + scope pattern (the primary Eloquent integration).
Use ClusterCast on the model, then wrap cluster filtering in a local scope so controllers never touch the parser directly:
public function scopeFilterCluster(Builder $query, array $filters): Builder
{
return ClusterService::apply($query, $filters); // illustrative
}
Keep the scope as the single boundary between HTTP input and the query engine. That makes it easy to swap the package out later, which matters given its maturity.
2. Service/facade pattern for reusable filters.
The ClusterService facade is the natural place to centralize filter definitions:
filterProducts, filterOrders).ClusterQuery.Builder (database path) or a ClusterVOCollection (memory path), but not both from the same method — keep return types predictable.3. In-memory filtering for loaded collections.
ClusterVOCollection lets you apply the same filter semantics after fetching:
$products = Product::all();
$filtered = $products
->pluck('attributes')
->filterByCluster($filters); // illustrative
Use this for:
Beware divergence: in-memory aggregation functions (documented section 17) may not behave identically to SQL aggregation (section 13), especially around nulls and empty arrays. Test both paths with the same fixtures.
4. Driver-aware SQL generation. The package documents SQL generation for multiple drivers plus custom SQLite functions (sections 9–10). Practical workflow:
testing connection, or your test suite will silently fall back to different behavior.EXISTS semantics are exactly where drivers diverge.5. Macros for terse syntax (section 12). If the package registers Laravel macros, use them in ad-hoc places (tinker, seeders, admin tooling) but prefer explicit service calls in application code. Macros are harder to grep and harder to version-pin when the package changes.
6. Arrays, sub-conditions, and EXISTS/NOT_EXISTS.
For array-valued keys (tags, line items), model the filter as:
EXISTS when you need “contains at least one element matching these sub-conditions”,NOT_EXISTS for “contains no element matching”,LIKE/NOT_LIKE for scalar string matching inside the cluster.Keep sub-conditions shallow and explicit. Deeply nested ASTs are where debugging cost explodes.
7. Aggregations.
Use SQL aggregation (section 13) when filtering at the database boundary and in-memory aggregation (section 17) when working with loaded ClusterVOCollections. Do not mix the two in one code path; pick a side per endpoint.
8. Extension points. Sections 18–19 document custom functions and parser/AST access. Two safe extension patterns:
Maturity is the biggest gotcha. Zero stars, zero dependents, French-only documentation, and a README that is more architecture tour than API reference. Pin an exact version in composer.json, read the source for any method you rely on, and be prepared to fork. Do not put this on a critical path without a fallback.
Documentation is conceptual, not contractual. The README describes components and capabilities but the examples here are reconstructed. Before writing application code, open ClusterQuery and ClusterService and confirm actual namespaces, method names, and argument order.
JSON column performance. Filtering JSON is not free. If a filter is hot, consider a generated/virtual column with an index (MySQL) or an expression index (Postgres), and keep the cluster filter for the long tail of ad-hoc keys. The package abstracts SQL generation; it does not make unindexed JSON scans fast.
SQLite custom functions must be registered everywhere. The package’s SQLite support depends on custom functions (section 10). Register them in a service provider that runs for every connection, including phpunit.xml’s DB_CONNECTION=sqlite / :memory: connection. Otherwise tests pass locally and fail in CI, or vice versa.
Driver parity is not guaranteed. Operators like LIKE, EXISTS, and aggregation functions compile differently per driver. Maintain a small parity test suite: same fixtures, same filter, assert identical IDs on SQLite and your production driver.
Cast edge cases. ClusterCast will encounter null, legacy scalar JSON, and malformed payloads. Decide explicitly: does null become an empty ClusterVO or stay null? Write a migration/backfill if the cast expects a strict shape. Test casting a raw array, a JSON string, and null.
Validate before parsing. If filters come from HTTP, whitelist field names, operators, and nesting depth before they reach ClusterQuery. The parser/AST (section 19) is powerful; do not feed it unvalidated user input. Treat operator names as an enum, not free strings.
Debugging workflow. When a filter returns unexpected results:
Macro registration. If macros are registered by the package’s service provider, confirm the provider is auto-discovered (or manually registered) in config/app.php / bootstrap/providers.php. Double registration in tests is a common source of “macro already defined” noise.
Extension points are your safety valve. Custom functions (section 18) and the AST layer (section 19) are where you should absorb gaps in the package rather than patching vendor files. Keep extensions in app/Support/Cluster/ so an upgrade or fork is a contained change.
Version pinning and changelog. With no release history or dependents, assume breaking changes. Pin "andydefer/laravel-cluster": "x.y.z" exactly, and review the diff before bumping. If you adopt it broadly, consider vendoring or forking under your own namespace so upstream changes cannot break production silently.
How can I help you explore Laravel packages today?