blackcube / yii-elastic
PHP 8.3+ dynamic model attributes from JSON Schema for Yii3 framework — EAV without the pain
Installs: 5
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/blackcube/yii-elastic
Requires
- php: >=8.3
- ext-intl: *
- ext-json: *
- blackcube/yii-magic-compose: ^1.0
- swaggest/json-schema: ^0.12.43
- yiisoft/active-record: ^1.0
- yiisoft/db: ^2.0
- yiisoft/validator: ^2.5
Requires (Dev)
- codeception/codeception: ^5.3
- codeception/module-asserts: ^3.3
- codeception/module-db: ^3.2
- 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.1
This package is auto-updated.
Last update: 2026-01-25 15:20:37 UTC
README
⚠️ Blackcube Warning
This is not EAV. If you want Entity-Attribute-Value with JOIN hell, look elsewhere.
Elastic stores JSON, validates with JSON Schema, and lets you query virtual columns. You manipulate PHP properties. You never see the JSON.
PHP 8.3+ dynamic model attributes from JSON Schema for Yii3 framework.
Installation
composer require blackcube/yii-elastic
Requirements
- PHP >= 8.3
- MySQL/MariaDB (for JSON column support)
Why Elastic?
| Approach | Problem |
|---|---|
| One table per type | 20 types = 20 tables, duplicated code |
| Catch-all columns | "field23 is what again?" |
| Raw HTML | Not validatable, not queryable, XSS |
| EAV | JOIN on JOIN on JOIN |
| Elastic | None of the above |
You manipulate PHP properties. Elastic handles JSON underneath.
Validation is automatic. JSON Schema → Yii3 Validator rules.
Queries are transparent. ->where(['virtualColumn' => 'value']) just works.
Evolution without migration. Add a field to the schema. No SQL migration needed.
How It Works
Storage
| Column | Purpose |
|---|---|
elasticSchemaId |
FK to elasticSchemas table |
_extras |
JSON data storage |
The developer never touches _extras directly. Properties are accessed like regular PHP properties.
Column names can be tuned
Override these methods in your model to use different column names:
public function elasticColumn(): string { return 'data'; } // Default: '_extras' public function elasticSchemaColumn(): string { return 'schemaId'; } // Default: 'elasticSchemaId'
Database Setup
1. Create the schemas table
Run the provided migration:
use Blackcube\Elastic\Migrations\M000000000000CreateElasticSchemas; $migration = new M000000000000CreateElasticSchemas(); $migration->up($builder);
2. Add columns to your table
CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, elasticSchemaId INT, _extras TEXT, FOREIGN KEY (elasticSchemaId) REFERENCES elasticSchemas(id) );
Quick Start
1. Create a JSON Schema
use Blackcube\Elastic\ElasticSchema; $schema = new ElasticSchema(); $schema->setName('ProductAttributes'); $schema->setSchema(json_encode([ 'type' => 'object', 'properties' => [ 'sku' => ['type' => 'string', 'minLength' => 3], 'price' => ['type' => 'number', 'minimum' => 0], 'inStock' => ['type' => 'boolean'], ], 'required' => ['sku'], ])); $schema->save();
2. Create your ActiveRecord model
<?php declare(strict_types=1); namespace App\Model; use Blackcube\Elastic\ElasticInterface; use Blackcube\Elastic\ElasticTrait; use Blackcube\MagicCompose\ActiveRecord\MagicComposeActiveRecordTrait; use Yiisoft\ActiveRecord\ActiveRecord; class Product extends ActiveRecord implements ElasticInterface { use MagicComposeActiveRecordTrait; use ElasticTrait; protected string $name = ''; public function tableName(): string { return 'products'; } public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } }
Usage
Working with dynamic attributes
// Create — properties are PHP, not JSON $product = new Product(); $product->setName('Laptop'); $product->elasticSchemaId = $schemaId; $product->sku = 'LAP-001'; // Virtual property $product->price = 999.99; // Virtual property $product->inStock = true; // Virtual property $product->insert(); // Read — same thing $loaded = Product::query()->where(['id' => $product->id])->one(); echo $loaded->sku; // 'LAP-001' echo $loaded->price; // 999.99 echo $loaded->inStock; // true // Update — still PHP $loaded->price = 899.99; $loaded->update();
Querying virtual columns
ElasticQuery transforms virtual columns to JSON_VALUE() expressions automatically:
// Filter by virtual column $products = Product::query() ->where(['sku' => 'LAP-001']) ->all(); // Multiple conditions $products = Product::query() ->where(['inStock' => true]) ->andWhere(['>', 'price', 500]) ->all(); // Order by virtual column $products = Product::query() ->orderBy(['price' => SORT_DESC]) ->all(); // Mix real and virtual columns $products = Product::query() ->where(['name' => 'Laptop', 'inStock' => true]) ->orderBy(['price' => SORT_ASC]) ->all();
Validating elastic attributes
use Blackcube\Elastic\Validator\ElasticRuleResolver; use Yiisoft\Validator\Validator; $resolver = new ElasticRuleResolver(); $rules = $resolver->resolve($product); $validator = new Validator(); $result = $validator->validate($product->getElasticValues(), $rules); if (!$result->isValid()) { foreach ($result->getErrors() as $error) { echo $error->getMessage(); } }
Supported JSON Schema features
| JSON Schema | Yii3 Validator Rule |
|---|---|
type: string |
StringValue |
type: integer |
Integer |
type: number |
Number |
type: boolean |
BooleanValue |
minimum, maximum |
Integer/Number with constraints |
minLength, maxLength |
Length |
pattern |
Regex |
enum |
In |
format: email |
Email |
format: idn-email |
Email with IDN |
format: url |
Url |
format: ipv4, format: ipv6 |
Ip |
required |
Required |
Labels, hints, placeholders from schema
JSON Schema metadata is extracted automatically:
| JSON Schema field | Method |
|---|---|
title |
getPropertyLabel($property) |
description |
getPropertyHint($property) |
placeholder |
getPropertyPlaceholder($property) |
Let's be honest
Performance on complex queries
JSON_VALUE() is slower than a native indexed column. Filtering 100,000 rows on a JSON field will be slow.
In practice: A CMS with a few thousand contents? No problem. A search engine on millions of rows? Use Elasticsearch or a real column.
No foreign keys in JSON
You can't JOIN on a JSON value. If you need relations, use real columns.
One-way compatibility
Adding optional fields: ✓ works, old data returns null.
Removing fields: data stays in database, but property is no longer accessible.
Rules
- Never modify
_extrasdirectly — use dynamic properties - Link your model to a schema — set
elasticSchemaIdbefore using elastic attributes - Use
ElasticQuery— thequery()method returns it automatically via the trait
License
BSD-3-Clause. See LICENSE.md.
Author
Philippe Gaultier philippe@blackcube.io