blackcube / active-record
Scoped queries, elastic dynamic attributes, Hazeltree nested sets — a toolkit for ActiveRecord
Requires
- php: ^8.0
- ext-intl: *
- swaggest/json-schema: ^0.12.43
- yiisoft/active-record: ^1.0
- yiisoft/cache: ^3.2
- yiisoft/db: ^2.0
- yiisoft/validator: ^2.5
Requires (Dev)
- blackcube/magic-compose: ^1.0.0
- codeception/codeception: ^5.3
- codeception/module-asserts: ^3.3
- codeception/module-db: ^3.2
- phpcompatibility/php-compatibility: ^9.3
- vlucas/phpdotenv: ^5.6
- yiisoft/cache-file: ^3.2
- yiisoft/db-migration: ^2.0
- yiisoft/db-mysql: ^2.0
- yiisoft/di: ^1.4
- yiisoft/event-dispatcher: ^1.1
- yiisoft/factory: ^1.3
- yiisoft/test-support: ^3.2
Suggests
- blackcube/magic-compose: Required to use MagicComposeActiveRecordTrait for AR extension chain
This package is auto-updated.
Last update: 2026-03-21 17:29:31 UTC
README
Scoped queries, elastic dynamic attributes, Hazeltree nested sets — a toolkit for ActiveRecord.
Quickstart
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();
Three building blocks
Scoped queries
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.
Elastic — Dynamic JSON Schema attributes
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);
Hazeltree — Nested sets with rational numbers
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();
Combined — Elastic + Hazeltree
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 }
Trait architecture
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.
Tests
vendor/bin/codecept run
500 tests, 2726 assertions across 17 suites.
Documentation
- Overview & prerequisites
- Installation
- API — Scopes & Qualification
- API — Elastic
- API — Hazeltree
- Integration
License
BSD-3-Clause. See LICENSE.md.
Author
Philippe Gaultier philippe@blackcube.io