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.
- π‘οΈ 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.
-
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.
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.
Use the Artisan command to scaffold your class.
php artisan make:json ProductAttributesThis creates app/Json/ProductAttributes.php.
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',
];
}
}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,
];
}Now you can use it in your Controllers, Services, or Jobs.
$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();$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 automaticallyBy 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.
The $casts property works similarly to Eloquent casts but for internal JSON keys.
Supported types: int, integer, float, real, double, string, bool, boolean, array.
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)Implement ValidatesJson and define rules(). Call $object->validate() anywhere. It uses Laravel's Validator under the hood, so all standard rules work.
Include TracksDirtyAttributes.
dirty(): Returns array of changed keys and new values.syncOriginal(): Called automatically on fetch, resets the baseline.
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.
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();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 |
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);
});- 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.
The MIT License (MIT). Please see License File for more information.