juanparati / embed-models
Embed sub-models inside JSON fields in Laravel Eloquent models with full casting, validation, and mass assignment support
Installs: 29
Dependents: 0
Suggesters: 0
Security: 0
Stars: 3
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/juanparati/embed-models
Requires
- php: ^8.2
- illuminate/database: ^12.0
- illuminate/support: ^12.0
- illuminate/validation: ^12.0
Requires (Dev)
- laravel/pint: ^1.25
- mockery/mockery: ^1.5
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.1
This package is auto-updated.
Last update: 2025-10-24 10:55:50 UTC
README
Eloquent embed models
A Laravel package that allows to embed sub-models inside JSON fields in Eloquent models. These embedded models behave like full Eloquent models with attributes, guards and casts.
Features
- Model Behavior: Embedded models support attributes, accessors, mutators, casts, fillable/guarded.
- Validation: Validate embedded model attributes on write with Laravel's validation rules.
- Nested Support: Embed models within embedded models.
- Collections: Handle arrays of embedded models with collection-like interfaces.
- Array Access: Access embedded model attributes like arrays.
- Type Casting: Support for all Eloquent casting types (dates, booleans, integers, etc.).
- No Persistence Methods: Embedded models don't save/delete - they're saved via the parent model.
- JSON raw: Save embedded models as JSON raw strings (not objects serialization).
- Transparent hydration: Inject your nested embed-model directly from a post request or array.
Installation
composer require juanparati/embed-models
Basic usage
1. Define your embedded model
use Juanparati\EmbedModels\EmbedModel; class Address extends EmbedModel { protected $fillable = ['street', 'city', 'zip', 'country']; protected function casts() { return [ 'verified' => 'boolean', ]; } protected function rules() { return [ 'zip' => 'required|regex:/^\d{5}$/', 'street' => 'required|string', ]; } }
Note: All embed-models attributes are fillable by default in opposition to normal Eloquent models.
You can define default values for attributes, so the embed will have a basic structure:
use Juanparati\EmbedModels\EmbedModel; class Address extends EmbedModel { protected $attributes = [ 'street' => '', 'city' => 'Aarhus', 'zip' => '8200', 'country' => 'Denmark', ]; }
2. Use it in your eloquent model
use Illuminate\Database\Eloquent\Model; use Juanparati\EmbedModels\Casts\AsEmbeddedModel; class Order extends Model { protected function casts() { return [ 'shipping_address' => AsEmbeddedModel::of(Address::class), ]; } }
3. Work with embedded models
// Create an order with an embedded address $order = new Order(); $order->shipping_address = new Address([ 'street' => '123 Main St', 'city' => 'Springfield', 'zip' => '12345', ]); $order->save(); // Modify the embedded model $order->shipping_address->street = '456 Oak Ave'; $order->save(); // Access via array syntax $order->shipping_address['city'] = 'New Springfield'; // Access attributes echo $order->shipping_address->street; // "456 Oak Ave"
Collections of embedded models
1. Define your collection
use Juanparati\EmbedModels\EmbedModel; use Juanparati\EmbedModels\EmbedCollection; class LineItem extends EmbedModel { protected function casts() { return [ 'quantity' => 'integer', 'price' => 'float', ]; } protected function rules() { return [ 'quantity' => 'required|integer|min:1', 'price' => 'required|numeric|min:0', ]; } } class LineItemCollection extends EmbedCollection { protected function getDefaultModelClass() { return LineItem::class; } }
2. Use in your model
use Juanparati\EmbedModels\Casts\AsEmbedCollection; class Order extends Model { protected function casts() { return [ 'line_items' => AsEmbedCollection::of(LineItemCollection::class), ]; } }
3. Work with collections
$order = new Order(); $order->line_items = new LineItemCollection([ ['sku' => 'ABC', 'quantity' => 5, 'price' => 10.00], ['sku' => 'DEF', 'quantity' => 3, 'price' => 15.00], ]); // Add items $order->line_items[] = new LineItem(['sku' => 'GHI', 'quantity' => 2, 'price' => 20.00]); $order->line_items->push(['sku' => 'JKL', 'quantity' => 1, 'price' => 25.00]); // Access items echo $order->line_items[3]->sku; // "JKL" // Use collection methods $total = $order->line_items->sum(fn($item) => $item->quantity * $item->price); // Filter $expensive = $order->line_items->filter(fn($item) => $item->price > 15); $order->save();
You can also transparently append arrays to embed collections, and they are automatically getting converted to embedded models:
$order = new Order(); $order->line_items = [ ['sku' => 'ABC', 'quantity' => 5, 'price' => 10.00], ['sku' => 'DEF', 'quantity' => 3, 'price' => 15.00], ]; // Add items $order->line_items[] = ['sku' => 'GHI', 'quantity' => 2, 'price' => 20.00]; $order->line_items[] = ['sku' => 'JKL', 'quantity' => 1, 'price' => 25.00]; // Access items echo $order->line_items[3]->sku; // "JKL"
Advanced features
Nested embedded models
class Coordinates extends EmbeddedModel { protected $casts = [ 'lat' => 'float', 'lng' => 'float', ]; } class Address extends EmbeddedModel { protected $casts = [ 'coordinates' => AsEmbedModel::of(Coordinates::class), ]; } // Usage $order->shipping_address = new Address([ 'street' => '123 Main St', 'coordinates' => ['lat' => 40.7128, 'lng' => -74.0060], ]); echo $order->shipping_address->coordinates->lat; // 40.7128
Accessors and mutators
class Address extends EmbeddedModel { protected function fullAddress(): \Illuminate\Database\Eloquent\Casts\Attribute { return Attribute::make( get: fn() => "{$this->street}, {$this->city}, {$this->zip}" ); } protected function zipAttribute(): \Illuminate\Database\Eloquent\Casts\Attribute { return Attribute::make( set: fn($value) => str_pad($value, 5, '0', STR_PAD_LEFT) ); } }
You can also use legacy accessors and mutators:
class Address extends EmbeddedModel { public function getFullAddressAttribute() { return "{$this->street}, {$this->city}, {$this->zip}"; } public function setZipAttribute($value) { return str_pad($value, 5, '0', STR_PAD_LEFT); } }
Mass assignment protection
Embed models are fillable by default in opposition of normal Eloquent models, but you can also still use guarded attributes:
class Address extends EmbeddedModel { protected $fillable = ['street', 'city', 'zip']; // or protected $guarded = ['internal_id']; } // This will ignore 'internal_id' $address = new Address([ 'street' => '123 Main St', 'internal_id' => 'secret', // ignored ]);
Validation
In case that validation rules are defined, they will be automatically applied to embedded models.
Validation runs automatically when you set attributes:
class Address extends EmbeddedModel { protected function rules() { return [ 'zip' => 'required|regex:/^\d{5}$/', 'email' => 'required|email', ]; } } // This will throw ValidationException $address = new Address(); $address->zip = 'invalid'; // Throws ValidationException
Type Casting
All Eloquent casting types are supported:
class Product extends EmbeddedModel { protected $casts = [ 'price' => 'float', 'quantity' => 'integer', 'active' => 'boolean', 'options' => 'array', 'released_at' => 'datetime', 'metadata' => 'json', ... ]; }
Behavior Notes
-
No Auto-Dirty Tracking: Modifying embedded models doesn't automatically mark the parent as dirty. You must explicitly call
$parent->save(). -
Null Initialization: If a JSON field is null in the database, accessing it returns
null, not an empty embedded model, however it's possible to initialize an embedded model using default attributes. -
Validation Timing: Validation runs when attributes are set, not when the parent model saves.
-
No Persistence Methods: Embedded models don't have
save(),update(), ordelete()methods. They're saved only through the parent model.