amadul / json-object
Structured, castable, schema-aware JSON objects for Eloquent models
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/amadul/json-object
Requires
- php: ^8.2
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
README
Structured, castable, schema-aware JSON objects for Eloquent models.
Transform your Eloquent JSON columns from messy arrays into strongly-typed, validated, and behavior-rich objects.
๐ Features
- ๐ก๏ธ Strongly Typed: Define classes for your JSON structures.
- ๐ Eloquent Casting: Seamless integration (feels like native attributes).
- โ Validation: Built-in Laravel validation logic.
- ๐ Dirty Tracking: Know exactly what changed inside the JSON.
- ๐ Logging: Track modifications automatically.
- ๐ Extensible: Plugin architecture via Macros.
- โก Artisan Integration:
make:jsoncommand for rapid development.
๐ฆ Installation
-
Require the package via Composer:
composer require amadul/json-object
-
Publish the configuration (Optional):
php artisan vendor:publish --tag=json-object-config
This creates
config/json-object.phpwhere you can customize paths and global feature flags.
๐ ๏ธ Full Implementation Guide: Step-by-Step
Let's build a real-world example: Product Attributes for an E-commerce system. We want to store color, size, and metadata in a single JSON column but interact with them like structured data.
Step 1: Create the JSON Object Class
Use the Artisan command to scaffold your class.
php artisan make:json ProductAttributes
This creates app/Json/ProductAttributes.php.
Step 2: Define Structure & Behavior
Open the generated file. We will:
- Define the Schema (allowed fields).
- Define Casts (data types).
- Add Traits for extra power (Accessors, Validation, Dirty Tracking).
namespace App\Json; use Amadul\JsonObject\JsonObject; use Amadul\JsonObject\Concerns\HasAccessors; use Amadul\JsonObject\Concerns\HasValidation; use Amadul\JsonObject\Concerns\TracksDirtyAttributes; use Amadul\JsonObject\Concerns\HasLogging; use Amadul\JsonObject\Contracts\ValidatesJson; class ProductAttributes extends JsonObject implements ValidatesJson { // 1. Add Capabilities use HasAccessors, HasValidation, TracksDirtyAttributes, HasLogging; /** * The whitelist of allowed attributes. * Any key not here will be filtered out on construction. */ protected array $schema = [ 'color', 'size', 'sku', 'tags', 'metadata.manufactured_at', ]; /** * Type casting for specific attributes. */ protected array $casts = [ 'size' => 'integer', 'tags' => 'array', 'metadata.manufactured_at' => 'datetime', // Custom casting can be added ]; /** * Validation rules using standard Laravel syntax. */ public function rules(): array { return [ 'color' => 'required|string|in:red,blue,green', 'size' => 'required|integer|min:0', 'sku' => 'required|alpha_dash', ]; } }
Step 3: Integrate with Eloquent Model
Open your Product model and cast the column (e.g., attributes or data) to your new class.
namespace App\Models; use Illuminate\Database\Eloquent\Model; use App\Json\ProductAttributes; class Product extends Model { protected $fillable = ['name', 'attributes']; protected $casts = [ 'attributes' => ProductAttributes::class, ]; }
Step 4: Usage in Application
Now you can use it in your Controllers, Services, or Jobs.
Creating & Saving
$product = new Product(); $product->name = 'T-Shirt'; // Initialize with array $product->attributes = [ 'color' => 'red', 'size' => '42', // Will be cast to int 42 'sku' => 'TS-RED-42' ]; // Validate before saving (Optional but recommended) try { $product->attributes->validate(); } catch (\Illuminate\Validation\ValidationException $e) { // Handle errors } $product->save();
Reading & Modifying
$product = Product::find(1); // Access with Dot Notation (via HasAccessors) echo $product->attributes->get('color'); // "red" echo $product->attributes->get('metadata.manufactured_at'); // Modify values $product->attributes->set('color', 'blue'); // Check dirty state (via TracksDirtyAttributes) if ($product->attributes->dirty()) { // Returns ['color' => 'blue'] // Log is automatically triggered if HasLogging is enabled } $product->save(); // Eloquent handles serialization automatically
๐ Deep Dive: Core Concepts
Schema Whitelisting
By defining protected $schema, you ensure strictly structured JSON. Any data passed to the constructor that isn't in the schema is discarded. This prevents "JSON pollution" with random keys.
Type Casting
The $casts property works similarly to Eloquent casts but for internal JSON keys.
Supported types: int, integer, float, real, double, string, bool, boolean, array.
Nested JSON Objects
You can cast attributes to other JsonObject classes, allowing for deep, structured JSON hierarchies.
// App/Json/ProductMeta.php class ProductMeta extends JsonObject { protected $schema = ['author', 'year']; } // App/Json/ProductAttributes.php class ProductAttributes extends JsonObject { protected $casts = [ 'meta' => ProductMeta::class, // Nested casting ]; } // Usage $product->attributes = [ 'color' => 'red', 'meta' => [ 'author' => 'John Doe', 'year' => 2023 ] ]; // Accessing nested objects echo $product->attributes->get('meta')->get('author'); // "John Doe" echo $product->attributes->get('meta.author'); // "John Doe" (via dot notation support)
Validation
Implement ValidatesJson and define rules(). Call $object->validate() anywhere. It uses Laravel's Validator under the hood, so all standard rules work.
Dirty Tracking
Include TracksDirtyAttributes.
dirty(): Returns array of changed keys and new values.syncOriginal(): Called automatically on fetch, resets the baseline.
Logging
Include HasLogging.
Configure the channel in config/json-object.php.
Use $this->log('message', ['data']) inside your JSON object methods to create audit trails of attribute changes.
๐ Advanced: Plugins & Extensibility
The JsonObject class is Macroable. You can add methods at runtime, which is perfect for plugins or app-wide extensions.
Example: Add an approve() method to all JSON objects.
// AppServiceProvider.php use Amadul\JsonObject\JsonObject; public function boot() { JsonObject::macro('approve', function () { $this->set('status', 'approved'); $this->set('approved_at', now()->toIso8601String()); return $this; }); } // Usage $product->attributes->approve();
โ๏ธ Configuration
config/json-object.php
| Key | Description | Default |
|---|---|---|
path |
Directory for generated classes | app_path('Json') |
namespace |
Namespace for generated classes | App\Json |
features.validation |
Enable/Disable validation trait helpers | true |
features.dirty_tracking |
Enable/Disable dirty tracking | true |
features.logging |
Global logging toggle | false |
log_channel |
Laravel log channel to use | stack |
๐งช Testing Your JSON Objects
Since these are standard PHP classes, unit testing is straightforward.
use App\Json\ProductAttributes; it('validates product attributes', function () { $attr = ProductAttributes::from(['color' => 'invalid-color']); expect(fn() => $attr->validate()) ->toThrow(\Illuminate\Validation\ValidationException::class); }); it('casts size to integer', function () { $attr = ProductAttributes::from(['size' => '42']); expect($attr->get('size'))->toBe(42); });
๐ค Contributing
- Fork the repo.
- Create your feature branch (
git checkout -b feature/amazing-feature). - Commit your changes (
git commit -m 'Add amazing feature'). - Push to the branch (
git push origin feature/amazing-feature). - Open a Pull Request.
๐ License
The MIT License (MIT). Please see License File for more information.