# Laravel Schemaless Attributes Laravel Schemaless Attributes is a package by Spatie that brings NoSQL-style flexibility to Eloquent models by allowing you to store arbitrary key-value pairs in a single JSON database column. This eliminates the need for complex database migrations when you need to add dynamic attributes to your models, making it ideal for scenarios where your data structure needs to evolve frequently or varies between records. The package provides a custom cast, trait, and query scope that seamlessly integrate with Laravel's Eloquent ORM. You can interact with schemaless attributes using both object property notation and array access, leverage dot notation for nested values, and query models based on their schemaless attributes. The package automatically handles JSON serialization and works with MySQL 5.7+ or any database that supports JSON columns. ## Installation and Setup ### Installing the Package ```bash composer require spatie/laravel-schemaless-attributes ``` ### Creating the Migration ```php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up() { Schema::table('products', function (Blueprint $table) { // Uses the schemalessAttributes() macro provided by the package $table->schemalessAttributes('extra_attributes'); // This is equivalent to: $table->json('extra_attributes')->nullable(); }); } public function down() { Schema::table('products', function (Blueprint $table) { $table->dropColumn('extra_attributes'); }); } }; ``` ### Configuring the Model (Single Column) ```php use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; class Product extends Model { protected $guarded = []; // Cast the JSON column to SchemalessAttributes public $casts = [ 'extra_attributes' => SchemalessAttributes::class, ]; // Create a query scope for filtering by schemaless attributes public function scopeWithExtraAttributes(): Builder { return $this->extra_attributes->modelScope(); } } ``` ### Configuring the Model (Multiple Columns) ```php use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; class Product extends Model { use SchemalessAttributesTrait; protected $guarded = []; // Define multiple schemaless attribute columns protected $schemalessAttributes = [ 'extra_attributes', 'meta_data', ]; public function scopeWithExtraAttributes(): Builder { return $this->extra_attributes->modelScope(); } public function scopeWithMetaData(): Builder { return $this->meta_data->modelScope(); } } ``` ## Setting Schemaless Attributes ### Setting Individual Attributes (Property Access) ```php $product = Product::create(['name' => 'Laptop']); // Set using object property notation $product->extra_attributes->color = 'silver'; $product->extra_attributes->weight = 1.5; $product->extra_attributes->in_stock = true; // Save to persist changes $product->save(); echo $product->extra_attributes->color; // 'silver' echo $product->extra_attributes->weight; // 1.5 var_dump($product->extra_attributes->in_stock); // bool(true) ``` ### Setting Individual Attributes (Array Access) ```php $product = Product::create(['name' => 'Phone']); // Set using array notation $product->extra_attributes['color'] = 'black'; $product->extra_attributes['storage'] = '256GB'; $product->save(); echo $product->extra_attributes['color']; // 'black' echo $product->extra_attributes['storage']; // '256GB' ``` ### Setting Multiple Attributes at Once ```php $product = Product::create(['name' => 'Tablet']); // Replace all schemaless attributes with a new array $product->extra_attributes = [ 'color' => 'space gray', 'screen_size' => 11, 'specs' => [ 'processor' => 'M1', 'ram' => '8GB', ], ]; $product->save(); // Or use the set() method to merge with existing attributes $product->extra_attributes->set([ 'warranty_years' => 2, 'certified_refurbished' => false, ]); $product->save(); print_r($product->extra_attributes->all()); // Array( // [color] => space gray // [screen_size] => 11 // [specs] => Array([processor] => M1, [ram] => 8GB) // [warranty_years] => 2 // [certified_refurbished] => // ) ``` ### Setting Nested Attributes with Dot Notation ```php $product = Product::create(['name' => 'Monitor']); // Set nested values using dot notation $product->extra_attributes->set('display.resolution', '4K'); $product->extra_attributes->set('display.refresh_rate', 144); $product->extra_attributes->set('display.panel_type', 'IPS'); $product->save(); echo $product->extra_attributes->get('display.resolution'); // '4K' echo $product->extra_attributes->get('display.refresh_rate'); // 144 ``` ### Creating Model with Schemaless Attributes ```php // Create a model with schemaless attributes in one statement $product = Product::create([ 'name' => 'Keyboard', 'extra_attributes' => [ 'type' => 'mechanical', 'layout' => 'full-size', 'switches' => 'Cherry MX Blue', 'backlight' => true, ], ]); echo $product->extra_attributes->type; // 'mechanical' echo $product->extra_attributes->switches; // 'Cherry MX Blue' ``` ## Getting Schemaless Attributes ### Getting Individual Attributes ```php $product = Product::find(1); // Direct property access $color = $product->extra_attributes->color; // Array access $weight = $product->extra_attributes['weight']; // Using get() method (returns null if not exists) $dimensions = $product->extra_attributes->get('dimensions'); // Using get() with default value $warranty = $product->extra_attributes->get('warranty_years', 1); echo $warranty; // Returns 1 if 'warranty_years' doesn't exist ``` ### Getting Nested Attributes with Dot Notation ```php $product = Product::create([ 'name' => 'Smart Watch', 'extra_attributes' => [ 'specs' => [ 'display' => [ 'size' => '1.9 inches', 'type' => 'AMOLED', ], 'battery' => '450mAh', ], ], ]); // Access nested values with dot notation echo $product->extra_attributes->get('specs.display.size'); // '1.9 inches' echo $product->extra_attributes->get('specs.display.type'); // 'AMOLED' echo $product->extra_attributes->get('specs.battery'); // '450mAh' // With default value for nested attribute $waterproof = $product->extra_attributes->get('specs.waterproof', false); ``` ### Getting Nested Arrays with Wildcards ```php $product = Product::create([ 'name' => 'Camera Bundle', 'extra_attributes' => [ 'items' => [ ['name' => 'Camera Body', 'price' => 1200], ['name' => 'Lens 50mm', 'price' => 300], ['name' => 'Memory Card', 'price' => 50], ], ], ]); // Get all values matching wildcard pattern $itemNames = $product->extra_attributes->get('items.*.name'); print_r($itemNames); // ['Camera Body', 'Lens 50mm', 'Memory Card'] $itemPrices = $product->extra_attributes->get('items.*.price'); print_r($itemPrices); // [1200, 300, 50] ``` ### Getting All Schemaless Attributes ```php $product = Product::find(1); // Get all attributes as an array $allAttributes = $product->extra_attributes->all(); print_r($allAttributes); // Or use toArray() (same result) $array = $product->extra_attributes->toArray(); // Convert to JSON $json = $product->extra_attributes->toJson(); echo $json; // {"color":"silver","weight":1.5} ``` ## Checking and Manipulating Attributes ### Checking if Attributes Exist ```php $product = Product::create([ 'name' => 'Headphones', 'extra_attributes' => [ 'color' => 'black', 'wireless' => true, ], ]); // Check if attribute exists if ($product->extra_attributes->has('color')) { echo "Color is set: " . $product->extra_attributes->color; } // Using isset if (isset($product->extra_attributes['wireless'])) { echo "Wireless feature is defined"; } // Check if empty if (empty($product->extra_attributes->battery_hours)) { echo "Battery hours not specified"; } ``` ### Forgetting (Removing) Attributes ```php $product = Product::create([ 'name' => 'Mouse', 'extra_attributes' => [ 'color' => 'white', 'dpi' => 16000, 'wireless' => true, 'weight' => 85, ], ]); // Remove a single attribute $product->extra_attributes->forget('weight'); $product->save(); // Remove using array unset unset($product->extra_attributes['wireless']); $product->save(); // Remove nested attribute using dot notation $product->extra_attributes->set('sensor.type', 'optical'); $product->extra_attributes->set('sensor.max_dpi', 16000); $product->extra_attributes->forget('sensor.max_dpi'); print_r($product->extra_attributes->all()); // Array([color] => white, [dpi] => 16000, [sensor] => Array([type] => optical)) ``` ### Updating Nested Attributes with Wildcards ```php $product = Product::create([ 'name' => 'Cable Pack', 'extra_attributes' => [ 'cables' => [ ['type' => 'USB-C', 'discounted' => false], ['type' => 'Lightning', 'discounted' => false], ['type' => 'Micro-USB', 'discounted' => false], ], ], ]); // Update all matching values using wildcards $product->extra_attributes->set('cables.*.discounted', true); $product->save(); $discounts = $product->extra_attributes->get('cables.*.discounted'); print_r($discounts); // [true, true, true] ``` ### Counting Attributes ```php $product = Product::create([ 'name' => 'Laptop Stand', 'extra_attributes' => [ 'material' => 'aluminum', 'adjustable' => true, 'foldable' => true, ], ]); $count = count($product->extra_attributes); echo "Number of extra attributes: $count"; // 3 // Add more attributes $product->extra_attributes->max_height = '20cm'; echo count($product->extra_attributes); // 4 ``` ## Using Collection Methods ### Calling Laravel Collection Methods ```php $product = Product::create([ 'name' => 'Accessory Bundle', 'extra_attributes' => [ 'item1' => 'Mouse', 'item2' => 'Keyboard', 'item3' => 'Mousepad', 'item4' => 'Cable', ], ]); // Use slice() to get a subset $subset = $product->extra_attributes->slice(1, 2)->toArray(); print_r($subset); // ['item2' => 'Keyboard', 'item3' => 'Mousepad'] // Use only() to get specific keys $selected = $product->extra_attributes->only('item1', 'item3')->toArray(); print_r($selected); // ['item1' => 'Mouse', 'item3' => 'Mousepad'] // Use pop() to remove and return last item $lastItem = $product->extra_attributes->pop(); echo $lastItem; // 'Cable' print_r($product->extra_attributes->all()); // ['item1' => 'Mouse', 'item2' => 'Keyboard', 'item3' => 'Mousepad'] ``` ### Aggregating Numeric Values ```php $order = Product::create([ 'name' => 'Multi-Item Order', 'extra_attributes' => [ ['name' => 'Item A', 'price' => 100, 'quantity' => 2], ['name' => 'Item B', 'price' => 50, 'quantity' => 3], ['name' => 'Item C', 'price' => 25, 'quantity' => 1], ], ]); // Calculate sum using Collection's sum method $totalPrice = $order->extra_attributes->sum('price'); echo "Total price: $$totalPrice"; // Total price: $175 // The schemaless attributes remain unchanged after collection methods print_r($order->extra_attributes->toArray()); // Still contains all three items with original data ``` ## Querying Models by Schemaless Attributes ### Basic Query with Multiple Attributes ```php // Create test products Product::create([ 'name' => 'Laptop A', 'extra_attributes' => [ 'color' => 'silver', 'ram' => '16GB', 'brand' => 'Apple', ], ]); Product::create([ 'name' => 'Laptop B', 'extra_attributes' => [ 'color' => 'silver', 'ram' => '32GB', 'brand' => 'Dell', ], ]); Product::create([ 'name' => 'Laptop C', 'extra_attributes' => [ 'color' => 'black', 'ram' => '16GB', 'brand' => 'HP', ], ]); // Find all products matching ALL specified attributes $silverWith16GB = Product::withExtraAttributes([ 'color' => 'silver', 'ram' => '16GB', ])->get(); echo $silverWith16GB->count(); // 1 (only Laptop A) echo $silverWith16GB->first()->name; // 'Laptop A' ``` ### Query with Single Attribute ```php // Find all products with a specific single attribute $silverProducts = Product::withExtraAttributes('color', 'silver')->get(); echo $silverProducts->count(); // 2 (Laptop A and Laptop B) foreach ($silverProducts as $product) { echo $product->name . " - " . $product->extra_attributes->color . "\n"; } // Output: // Laptop A - silver // Laptop B - silver ``` ### Query with Custom Operators ```php // Use custom operators like LIKE, !=, >, <, etc. $nonAppleProducts = Product::withExtraAttributes('brand', '!=', 'Apple')->get(); echo $nonAppleProducts->count(); // 2 (Laptop B and Laptop C) // LIKE operator for pattern matching Product::create([ 'name' => 'Phone X', 'extra_attributes' => ['model' => 'iPhone 15 Pro'], ]); Product::create([ 'name' => 'Phone Y', 'extra_attributes' => ['model' => 'iPhone 15'], ]); $iPhones = Product::withExtraAttributes('model', 'LIKE', 'iPhone%')->get(); echo $iPhones->count(); // 2 ``` ### Query with Nested Attributes ```php Product::create([ 'name' => 'Gaming Laptop', 'extra_attributes' => [ 'specs' => [ 'cpu' => 'Intel i9', 'gpu' => 'RTX 4080', ], ], ]); Product::create([ 'name' => 'Business Laptop', 'extra_attributes' => [ 'specs' => [ 'cpu' => 'Intel i7', 'gpu' => 'Integrated', ], ], ]); // Query using -> notation for nested attributes $gamingLaptops = Product::withExtraAttributes('specs->gpu', 'RTX 4080')->get(); echo $gamingLaptops->count(); // 1 echo $gamingLaptops->first()->name; // 'Gaming Laptop' // With operators on nested attributes $highEndCPU = Product::withExtraAttributes('specs->cpu', '!=', 'Integrated')->get(); echo $highEndCPU->count(); // 2 ``` ### Chaining Query Scopes ```php // Combine with regular Eloquent queries $results = Product::where('name', 'LIKE', 'Laptop%') ->withExtraAttributes('color', 'silver') ->orderBy('created_at', 'desc') ->limit(5) ->get(); // Chain multiple schemaless attribute filters $filtered = Product::withExtraAttributes('brand', 'Apple') ->where('created_at', '>', now()->subDays(30)) ->get(); ``` ## Reusable Trait Pattern ### Creating a Custom Trait for Multiple Models ```php namespace App\Models\Concerns; use Illuminate\Database\Eloquent\Builder; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; trait HasExtraAttributes { // Automatically register the cast when trait is initialized public function initializeHasExtraAttributes() { $this->casts['extra_attributes'] = SchemalessAttributes::class; } // Provide the query scope public function scopeWithExtraAttributes(): Builder { return $this->extra_attributes->modelScope(); } } ``` ### Using the Custom Trait in Models ```php use App\Models\Concerns\HasExtraAttributes; use Illuminate\Database\Eloquent\Model; class Product extends Model { use HasExtraAttributes; protected $guarded = []; } class Order extends Model { use HasExtraAttributes; protected $guarded = []; } class Customer extends Model { use HasExtraAttributes; protected $guarded = []; } // Now all three models have schemaless attributes automatically $product = Product::create(['name' => 'Widget']); $product->extra_attributes->featured = true; $product->save(); $customer = Customer::create(['email' => 'john@example.com']); $customer->extra_attributes->preferences = ['theme' => 'dark', 'notifications' => true]; $customer->save(); $featuredProducts = Product::withExtraAttributes('featured', true)->get(); ``` ## Advanced Usage Patterns ### Iterating Over Attributes ```php $product = Product::create([ 'name' => 'Multi-Feature Product', 'extra_attributes' => [ 'feature1' => 'GPS', 'feature2' => 'Bluetooth 5.0', 'feature3' => 'Water Resistant', 'feature4' => 'Fast Charging', ], ]); // Iterate as array foreach ($product->extra_attributes as $key => $value) { echo "$key: $value\n"; } // Output: // feature1: GPS // feature2: Bluetooth 5.0 // feature3: Water Resistant // feature4: Fast Charging // Use getIterator() $iterator = $product->extra_attributes->getIterator(); while ($iterator->valid()) { echo $iterator->key() . " => " . $iterator->current() . "\n"; $iterator->next(); } ``` ### Working with Complex Nested Structures ```php $product = Product::create([ 'name' => 'Smartphone', 'extra_attributes' => [ 'hardware' => [ 'display' => [ 'size' => 6.7, 'type' => 'OLED', 'resolution' => '2778x1284', ], 'camera' => [ 'front' => '12MP', 'rear' => ['main' => '48MP', 'ultra_wide' => '12MP', 'telephoto' => '12MP'], ], ], 'software' => [ 'os' => 'iOS 17', 'features' => ['FaceID', 'ApplePay', 'Siri'], ], ], ]); // Access deeply nested values echo $product->extra_attributes->get('hardware.display.type'); // 'OLED' echo $product->extra_attributes->get('hardware.camera.rear.main'); // '48MP' // Get array of nested values $features = $product->extra_attributes->get('software.features'); print_r($features); // ['FaceID', 'ApplePay', 'Siri'] // Update specific nested value $product->extra_attributes->set('hardware.display.resolution', '3000x1500'); $product->save(); ``` ### Type Preservation ```php $product = Product::create([ 'name' => 'Test Product', 'extra_attributes' => [ 'string_value' => 'hello', 'integer_value' => 42, 'float_value' => 19.99, 'boolean_true' => true, 'boolean_false' => false, 'null_value' => null, 'array_value' => [1, 2, 3], ], ]); $product->save(); $product->refresh(); // Types are preserved through database round-trip var_dump($product->extra_attributes->string_value); // string(5) "hello" var_dump($product->extra_attributes->integer_value); // int(42) var_dump($product->extra_attributes->float_value); // float(19.99) var_dump($product->extra_attributes->boolean_true); // bool(true) var_dump($product->extra_attributes->boolean_false); // bool(false) var_dump($product->extra_attributes->null_value); // NULL var_dump($product->extra_attributes->array_value); // array(3) { [0]=> int(1) [1]=> int(2) [2]=> int(3) } ``` ## Summary Laravel Schemaless Attributes provides an elegant solution for storing dynamic, schema-free data within traditional relational database models. This package is particularly useful for e-commerce applications with varying product attributes, multi-tenant systems with custom fields, or any application where data structure flexibility is required without constant schema migrations. The seamless integration with Eloquent means you can store metadata, preferences, custom properties, or any arbitrary data alongside your regular model attributes while maintaining the benefits of Laravel's ORM. The package supports all standard Laravel Collection methods, making it powerful for data manipulation, and includes query scopes that allow you to filter records based on schemaless attributes using standard SQL operators. Whether you need simple key-value storage or deeply nested JSON structures with wildcard querying, this package provides a robust, type-safe solution that works across MySQL, PostgreSQL, and any database supporting JSON columns. Its minimal configuration and intuitive API make it easy to add flexible attributes to any existing Laravel application.