Scoped queries, elastic dynamic attributes, Hazeltree nested sets — a toolkit for ActiveRecord.
composer require blackcube/active-record// Scoped queries
$products = Product::query()->active()->language(languageId: 'fr')->all();
// Elastic — dynamic attributes from JSON Schema
$article->author = 'Philippe';
$article->rating = 5;
$article->save();
$found = Article::query()->where(['author' => 'Philippe'])->all();
// Hazeltree — nested sets
$child = new Menu();
$child->name = 'About';
$child->saveInto($homepage);
$breadcrumb = $child->relativeQuery()->parent()->includeAncestors()->includeSelf()->all();Named, composable filters. PHP 8 named arguments flow through __call into typed parameters.
class ProductQuery extends ActiveQuery implements ScopableQueryInterface
{
use ScopableTrait;
use QualifyColumnTrait;
}
$products = Product::query()
->active(active: false)
->language(languageId: 'fr')
->andWhere(['like', 'title', 'laptop'])
->all();QualifyColumnTrait auto-prefixes column names with the table qualifier. Scopes write simple names, qualification is transparent. No ambiguous column errors in JOINs.
JSON column + JSON Schema = dynamic attributes without EAV.
class Article extends ActiveRecord implements ElasticInterface
{
use ElasticTrait; // handles __get/__set dispatch, no MagicCompose needed
}
// Properties come from JSON Schema, not PHP class definition
$article->author = 'Philippe'; // stored in _extras JSON column
$article->rating = 5;
$article->save();
// Query virtual columns — automatically converted to JSON_VALUE()
$top = Article::query()->where(['>', 'rating', 3])->orderBy(['rating' => SORT_DESC])->all();
// Validation from JSON Schema
$resolver = new ElasticRuleResolver();
$rules = $resolver->resolve($article);Tree structure in RDBMS. Read branches in one query. Write without global renumbering.
Based on Dan Hazel's research (2008).
class Menu extends ActiveRecord implements HazeltreeInterface
{
use HazeltreeTrait; // handles __get/__set dispatch, no MagicCompose needed
}
// Write
$home = new Menu(); $home->name = 'Home'; $home->save(); // path: 1
$about = new Menu(); $about->name = 'About'; $about->saveInto($home); // path: 1.1
$blog = new Menu(); $blog->name = 'Blog'; $blog->saveAfter($about); // path: 1.2
// Read — one query each
$children = $home->relativeQuery()->children()->all();
$breadcrumb = $about->relativeQuery()->parent()->includeAncestors()->includeSelf()->all();
$siblings = $about->relativeQuery()->siblings()->next()->all();
$roots = Menu::query()->roots()->all();For models that need both dynamic attributes and tree structure:
class Content extends ActiveRecord implements ElasticInterface, HazeltreeInterface
{
use HazeltreeElasticTrait; // dispatches to both, resolves all collisions
}Three layers, zero collision:
| Layer | Purpose | Example |
|---|---|---|
| Base traits | Prefixed methods, protected, no collision |
BaseElasticTrait, BaseHazeltreeTrait |
| Composite traits | __get/__set dispatch, one per model |
ElasticTrait, HazeltreeTrait, HazeltreeElasticTrait |
| Abstract classes | Convenience, just use Trait |
AbstractElasticActiveRecord, AbstractHazeltreeElasticQuery |
Base traits use tryElasticGet() / tryHazeltreeGet() returning bool. Composite traits chain: elastic, then hazeltree, then parent::. No insteadof, no MagicCompose needed.
vendor/bin/codecept run500 tests, 2726 assertions across 17 suites.
- Overview & prerequisites
- Installation
- API — Scopes & Qualification
- API — Elastic
- API — Hazeltree
- Integration
BSD-3-Clause. See LICENSE.md.
Philippe Gaultier philippe@blackcube.io