blackcube / yii-bridge-model
Bridged FormModel for Yii3 framework
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/blackcube/yii-bridge-model
Requires
- php: >=8.3
- ext-intl: *
- ext-json: *
- blackcube/elastic: ^1.0
- blackcube/hazeltree: ^1.0
- yiisoft/active-record: ^1.0
- yiisoft/db: ^2.0
- yiisoft/form-model: ^1.1
- yiisoft/hydrator: ^1.6
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.2
This package is auto-updated.
Last update: 2026-02-27 09:07:49 UTC
README
Bidirectional data bridge between FormModel and ActiveRecord for Yii3 framework.
Installation
composer require blackcube/yii-bridge-model
Why Bridge?
| Approach | Problem |
|---|---|
| Manual mapping | Boilerplate hell, copy-paste errors |
| Auto-hydration | Breaks on type mismatch, no control |
| Bridge | None of the above |
You declare mappings with attributes. Bridge handles the rest.
Type conversion is automatic. DateTimeImmutable ↔ string just works.
Scenarios filter what transfers. Edit form ≠ Create form ≠ API form.
Elastic properties are transparent. Dynamic JSON Schema fields work like regular properties.
How It Works
The Bridge Attribute
#[Bridge] // Auto-detect target getter/setter #[Bridge(name: 'title')] // Map to different name #[Bridge(format: 'Y-m-d')] // Date format for conversion #[Bridge(getter: 'getX', setter: 'setX')] // Explicit target methods
Data Flow
┌─────────────────────────────────────────────────────────────────┐
│ DATA FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ AR (native types) ──────────────────► FormModel (strings) │
│ createFromModel() │
│ initFromModel() │
│ │
│ FormModel (strings) ──────────────────► AR (native types) │
│ populateModel() │
│ │
│ #[Bridge] = mapping instruction │
│ Mapper = automatic DateTimeImmutable ↔ string conversion │
│ Scenarios = field filtering │
│ │
└─────────────────────────────────────────────────────────────────┘
Quick Start
1. Create your ActiveRecord
<?php declare(strict_types=1); namespace App\Model; use Yiisoft\ActiveRecord\ActiveRecord; class Product extends ActiveRecord { protected ?int $id = null; protected string $name = ''; protected ?float $price = null; protected bool $active = true; protected ?DateTimeImmutable $publishedAt = null; public function tableName(): string { return 'products'; } public function getId(): ?int { return $this->id; } public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } public function getPrice(): ?float { return $this->price; } public function setPrice(?float $price): void { $this->price = $price; } public function isActive(): bool { return $this->active; } public function setActive(bool $active): void { $this->active = $active; } public function getPublishedAt(): ?DateTimeImmutable { return $this->publishedAt; } public function setPublishedAt(?DateTimeImmutable $publishedAt): void { $this->publishedAt = $publishedAt; } }
2. Create your FormModel
<?php declare(strict_types=1); namespace App\Form; use Blackcube\BridgeModel\Attributes\Bridge; use Blackcube\BridgeModel\BridgeFormModel; class ProductForm extends BridgeFormModel { #[Bridge] public ?int $id = null; #[Bridge] public string $name = ''; #[Bridge] public ?float $price = null; #[Bridge] public bool $active = true; #[Bridge(format: 'Y-m-d')] public ?string $publishedAt = null; public function rules(): array { return [ 'name' => [new Required(), new Length(min: 3, max: 255)], 'price' => [new Number(min: 0)], ]; } }
Usage
Load from ActiveRecord
// Factory method — creates and populates $form = ProductForm::createFromModel($product); // Or manually $form = new ProductForm(); $form->initFromModel($product);
Save to ActiveRecord
// Load POST data $form->load($request->getParsedBody()); // Validate if ($form->validate()) { // Transfer to AR $form->populateModel($product); $product->save(); }
Scenarios
Control which fields are active for validation and transfer:
class ProductForm extends BridgeFormModel { public const SCENARIO_CREATE = 'create'; public const SCENARIO_EDIT = 'edit'; #[Bridge] public ?int $id = null; #[Bridge] public string $name = ''; #[Bridge] public ?float $price = null; public function scenarios(): array { return [ self::SCENARIO_CREATE => ['name', 'price'], self::SCENARIO_EDIT => ['name', 'price', 'id'], ]; } } // Usage $form = ProductForm::createFromModel($product); $form->setScenario(ProductForm::SCENARIO_EDIT);
Date Conversion
DateTimeImmutable ↔ string conversion is automatic when you specify a format:
// AR has DateTimeImmutable protected ?DateTimeImmutable $publishedAt = null; // FormModel has string with format #[Bridge(format: 'Y-m-d')] public ?string $publishedAt = null; // Transfer AR → FormModel: DateTimeImmutable becomes "2025-02-01" // Transfer FormModel → AR: "2025-02-01" becomes DateTimeImmutable
Type Override
When reflection can't detect the type (mixed, inherited, etc.):
#[Bridge(type: 'DateTimeImmutable', format: 'Y-m-d')] public ?string $createdAt = null;
Elastic Integration
Bridge works transparently with elastic dynamic properties.
AR with Elastic
class Product extends ActiveRecord implements ElasticInterface { use MagicComposeActiveRecordTrait; use ElasticTrait; // Regular properties... }
FormModel with Elastic
Elastic properties are auto-discovered from the JSON Schema:
class ProductForm extends BridgeFormModel { #[Bridge] public string $name = ''; // Elastic properties (sku, color, size) are added automatically // from the AR's JSON Schema when you call initFromModel() public function scenarios(): array { return [ self::SCENARIO_DEFAULT => ['name', self::ALL_ELASTIC_ATTRIBUTES], 'basic' => ['name', self::NO_ELASTIC_ATTRIBUTES], ]; } }
Elastic Constants
| Constant | Effect |
|---|---|
ALL_ELASTIC_ATTRIBUTES |
Include all elastic fields in scenario |
NO_ELASTIC_ATTRIBUTES |
Exclude all elastic fields from scenario |
Bridge Resolution
Bridge auto-detects target accessors in this order:
| Source | Target Detection Order |
|---|---|
Property $name |
setName() → getName()/isName() → $name |
Getter getName() |
setName() → $name |
Setter setName() |
getName()/isName() → $name |
Explicit attributes always win:
#[Bridge(getter: 'fetchTitle', setter: 'storeTitle')] public string $name = '';
API Reference
BridgeFormModel
| Method | Description |
|---|---|
createFromModel($model) |
Factory: create and populate from AR |
initFromModel($model) |
Populate from AR |
populateModel($model) |
Transfer to AR (filtered by scenario) |
load($data, $scope) |
Hydrate from array (POST data) |
validate() |
Validate with filtered rules |
setScenario($scenario) |
Set active scenario |
getScenario() |
Get active scenario |
getProperties() |
Get all Bridge components |
getRules() |
Get rules filtered by scenario |
Bridge Attribute
| Parameter | Type | Description |
|---|---|---|
name |
?string |
Canonical name (for fusion) |
type |
?string |
Override detected type |
format |
?string |
Date format for conversion |
property |
?string |
Explicit target property |
getter |
?string |
Explicit target getter |
setter |
?string |
Explicit target setter |
Let's be honest
No magic
Bridge doesn't guess. If you don't put #[Bridge], the property is ignored.
One AR per FormModel (usually)
Multi-AR forms are possible (call initFromModel() multiple times) but think twice. Complex forms often mean complex problems.
Elastic requires ElasticHydrator
Yii's default Hydrator doesn't handle elastics. Bridge includes ElasticHydrator that does.
Rules
- Always use
#[Bridge]— unmarked properties don't transfer - Specify format for dates — or get "Y-m-d H:i:s" default
- Use scenarios — don't transfer everything everywhere
- Always add rules — Bridge doesn't transfer Form -> AR if property has no validation rules
- Validate before populate — Bridge doesn't validate, it transfers
License
BSD-3-Clause. See LICENSE.md.
Author
Philippe Gaultier philippe@blackcube.io