PHPackages                             daikazu/laravel-meta - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. daikazu/laravel-meta

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

daikazu/laravel-meta
====================

This is my package laravel-meta

v0.1.0(1mo ago)0151MITPHPPHP ^8.3CI failing

Since Jun 6Pushed 1mo agoCompare

[ Source](https://github.com/daikazu/laravel-meta)[ Packagist](https://packagist.org/packages/daikazu/laravel-meta)[ Docs](https://github.com/daikazu/laravel-meta)[ GitHub Sponsors](https://github.com/Daikazu)[ RSS](/packages/daikazu-laravel-meta/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (12)Versions (2)Used By (1)

laravel Meta
============

[](#laravel-meta)

> ⚠️ **Work in progress.** This package is under active development and not yet stable. APIs may change without notice and it is not recommended for production use yet.

[![Latest Version on Packagist](https://camo.githubusercontent.com/6e7f37895cf9b9cc9eee00bd6b9386b7c1eb7fce24c0bb77a062bf84c94b0ff9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6461696b617a752f6c61726176656c2d6d6574612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/daikazu/laravel-meta)[![GitHub Tests Action Status](https://github.com/daikazu/laravel-meta/actions/workflows/run-tests.yml/badge.svg)](https://github.com/daikazu/laravel-meta/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/db7a82c2c313456cb30fbd8902a4e18cd4e24cfa2e94caeb05bbdfa0a0313053/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6461696b617a752f6c61726176656c2d6d6574612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/daikazu/laravel-meta)

Typed, casted, queryable key-value metadata for any Eloquent model.

- One `$model->meta` property gives you a full read/write/delete API.
- Dot-notation for nested JSON values.
- Built-in casts (`string`, `integer`, `float`, `boolean`, `array`, `json`, `collection`, `datetime`, `encrypted`) plus custom casts and zero-boilerplate plain value objects.
- Two storage drivers: separate `model_meta` table (default) or a JSON column on the model's own table.
- Query scopes: `whereMeta`, `whereMetaIn`, `whereMetaBetween`, `whereMetaNull`, `orderByMeta`, and more.
- Optional in-memory caching, event broadcasting, schema validation, computed meta, and an indexed sidecar table for hot keys.
- 64 tests (Pest), PHPStan max, Laravel 11 / 12 / 13.

---

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11, 12, or 13

---

Installation
------------

[](#installation)

```
composer require daikazu/laravel-meta
```

Run the install command to publish the config, publish the migrations, and optionally run them:

```
php artisan meta:install
```

The command publishes two tags:

- `laravel-meta-config` → `config/meta.php`
- `laravel-meta-migrations` → two migration stubs (`create_model_meta_table`, `create_model_meta_index_table`)

To force-overwrite existing published files:

```
php artisan meta:install --force
```

---

Model Setup
-----------

[](#model-setup)

Add the `HasMeta` trait to any Eloquent model:

```
use Daikazu\LaravelMeta\Concerns\HasMeta;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasMeta;
}
```

That's it. The `$model->meta` property is now a `MetaBag` instance.

### Optional: the `HasMetadata` interface

[](#optional-the-hasmetadata-interface)

The `HasMeta` trait already provides everything, but you can additionally implement the `HasMetadata` interface for stronger typing. It's purely additive — no behaviour changes — and lets static analysis and other packages (such as [`daikazu/filament-meta`](https://github.com/daikazu/filament-meta)) accept "a model with metadata" without falling back to `mixed`:

```
use Daikazu\LaravelMeta\Concerns\HasMeta;
use Daikazu\LaravelMeta\Contracts\HasMetadata;
use Illuminate\Database\Eloquent\Model;

class Product extends Model implements HasMetadata
{
    use HasMeta;
}
```

The interface declares a single method, `meta(): MetaBag` (and the `@property-read MetaBag $meta` accessor), both of which the trait already satisfies — so implementing it never requires extra code.

---

Basic Usage
-----------

[](#basic-usage)

### Reading

[](#reading)

```
$product->meta->get('color');                 // mixed, null when absent
$product->meta->get('color', 'red');          // with default

// Typed accessors — each returns null when the key is absent
$product->meta->string('title');              // ?string
$product->meta->integer('views');             // ?int
$product->meta->float('rating');              // ?float
$product->meta->boolean('featured');          // ?bool
$product->meta->array('tags');                // ?array
$product->meta->collection('tags');           // Collection
$product->meta->datetime('published_at');     // ?CarbonImmutable

// All keys at once
$product->meta->all();                        // array
```

### Writing

[](#writing)

```
$product->meta->set('color', 'blue');

// Multiple keys in one call (single DB round-trip for non-dot keys)
$product->meta->setMany([
    'color'    => 'blue',
    'featured' => true,
    'rating'   => 4.5,
]);

// Alias for setMany
$product->meta->upsert(['color' => 'green']);

// Sync — identical to setMany but removes keys not in the payload
$product->meta->sync(['color' => 'green', 'weight' => 1.2]);
// keys not listed ('featured', 'rating' above) are deleted
```

### Deleting

[](#deleting)

```
$product->meta->forget('color');
$product->meta->forgetMany(['color', 'rating']);
$product->meta->flush();   // remove every key for this model
```

### Existence

[](#existence)

```
$product->meta->has('color');     // bool
$product->meta->missing('color'); // bool
```

### Dot-Notation

[](#dot-notation)

Dot notation reads and writes into nested JSON values stored under the top-level key:

```
$product->meta->set('seo.open_graph.title', 'My Product');
$product->meta->get('seo.open_graph.title'); // 'My Product'
$product->meta->get('seo');                  // ['open_graph' => ['title' => 'My Product']]
```

---

Buffering on Unsaved Models
---------------------------

[](#buffering-on-unsaved-models)

Writing to `->meta` before the model is saved is safe. Values are buffered and flushed automatically inside the `saved` hook:

```
$product = new Product(['name' => 'New']);
$product->meta->set('featured', true);
$product->save();  // buffered meta is written here
```

---

Casting
-------

[](#casting)

### Declared Casts

[](#declared-casts)

Define `$metaCasts` on your model to control how values are serialised and hydrated:

```
class Product extends Model
{
    use HasMeta;

    protected array $metaCasts = [
        'featured'    => 'boolean',
        'rating'      => 'float',
        'published_at' => 'datetime',
        'tags'        => 'array',
        'seo'         => SeoData::class,   // value object (see below)
    ];
}
```

### Built-In Cast Aliases

[](#built-in-cast-aliases)

AliasNotes`string``integer` / `int``float` / `double``boolean` / `bool``array`PHP array, JSON-encoded at rest`json`Alias for `array``collection``Illuminate\Support\Collection``datetime``CarbonImmutable``encrypted`Encrypts the raw value using Laravel's encrypter### Custom Cast Classes

[](#custom-cast-classes)

Implement `Daikazu\LaravelMeta\Contracts\MetaCast`:

```
use Daikazu\LaravelMeta\Contracts\MetaCast;
use Illuminate\Database\Eloquent\Model;

class MoneyCast implements MetaCast
{
    public function get(mixed $value, string $key, Model $model): Money
    {
        return new Money((int) $value['amount'], (string) $value['currency']);
    }

    public function set(mixed $value, string $key, Model $model): array
    {
        return ['amount' => $value->amount, 'currency' => $value->currency];
    }
}
```

Then use the FQCN in `$metaCasts`:

```
protected array $metaCasts = [
    'price' => MoneyCast::class,
];
```

### The `Castable` Contract

[](#the-castable-contract)

For classes that produce their own cast, implement `Daikazu\LaravelMeta\Contracts\Castable`:

```
use Daikazu\LaravelMeta\Contracts\Castable;
use Daikazu\LaravelMeta\Contracts\MetaCast;

class Money implements Castable
{
    public static function castUsing(): MetaCast
    {
        return new MoneyCast;
    }
}
```

`CastManager` calls `::castUsing()` automatically when it sees a `Castable` class.

### Plain Value Objects (Zero Boilerplate)

[](#plain-value-objects-zero-boilerplate)

Any class that is **not** a `MetaCast` or `Castable` is automatically wrapped in `ValueObjectCast`, which serialises and hydrates via the constructor's public promoted properties. No custom cast class needed:

```
// The value object — no cast interface required
final readonly class SeoData
{
    public function __construct(
        public string $title,
        public ?string $description = null,
    ) {}
}

// Model
protected array $metaCasts = [
    'seo' => SeoData::class,
];

// Usage
$product->meta->set('seo', new SeoData(title: 'My Product', description: 'Great.'));
$seo = $product->meta->get('seo');  // SeoData instance
```

### Global Custom Casts via Config

[](#global-custom-casts-via-config)

Register additional aliases in `config/meta.php` so every model can use them by alias name:

```
'casts' => [
    'money' => \App\Casts\MoneyCast::class,
],
```

---

Encrypted Meta
--------------

[](#encrypted-meta)

Two equivalent ways to encrypt a value at rest:

```
// 1. Declare the cast
protected array $metaCasts = ['api_key' => 'encrypted'];

// 2. Fluent modifier (does not require a declared cast)
$product->meta->encrypted()->set('api_key', 'sk-...');
$product->meta->encrypted()->setMany(['token' => 'abc', 'secret' => 'xyz']);
```

The `encrypted()` modifier applies only to the immediately following `set()` or `setMany()` call.

---

Storage Drivers
---------------

[](#storage-drivers)

### Table Driver (Default)

[](#table-driver-default)

Stores each top-level key as a row in the `model_meta` table. This is the default and requires no per-model configuration.

```
config/meta.php → 'driver' => 'table'

```

### JSON Column Driver

[](#json-column-driver)

Stores all meta for a model in a single JSON column on the model's own table. Add the column and cast to your model:

```
// Migration snippet
$table->json('meta_data')->nullable();

// Model
class Page extends Model
{
    use HasMeta;

    protected string $metaColumn = 'meta_data';

    protected $casts = ['meta_data' => 'array'];
}
```

When `$metaColumn` is defined, the JSON driver is selected automatically regardless of the global `driver` config. You can also set `protected string $metaDriver = 'json'` explicitly, in which case `$metaColumn` falls back to the `json_column` config value (`meta_data` by default).

### Driver Resolution Order

[](#driver-resolution-order)

1. `protected string $metaDriver = '...'` on the model — explicit override.
2. `protected string $metaColumn = '...'` on the model — implies `json` driver.
3. `meta.driver` config key — global default (`table`).

---

Query Scopes
------------

[](#query-scopes)

All scopes are available on every model that uses `HasMeta`.

```
// Equality (shorthand: omit operator for '=')
Product::whereMeta('featured', true)->get();
Product::whereMeta('rating', '>', 4)->get();

// OR variant
Product::whereMeta('featured', true)
       ->orWhereMeta('featured', false)
       ->get();

// IN / NOT IN
Product::whereMetaIn('category', ['books', 'games'])->get();
Product::whereMetaNotIn('status', ['archived'])->get();

// NULL / NOT NULL (works with dot-notation keys)
Product::whereMetaNull('seo.description')->get();
Product::whereMetaNotNull('published_at')->get();

// BETWEEN
Product::whereMetaBetween('rating', [3, 5])->get();

// ORDER BY
Product::orderByMeta('rating', 'desc')->get();
```

---

Eager Loading
-------------

[](#eager-loading)

`withMeta()` is the canonical scope for eager-loading metadata. It loads the underlying `metaRecords` relation so the `MetaBag` can prime itself without additional queries:

```
$products = Product::withMeta()->get();
// Each $product->meta->get('...') now reads from memory.
```

**Why `withMeta()` and not `with('meta')`?**`$model->meta` is a computed property (accessor) that returns a `MetaBag` — it is not an Eloquent relation. The underlying polymorphic relation is `metaRecords`. `withMeta()` calls `->with('metaRecords')` under the hood and is the intended API.

```
// Also available directly when you need the raw relation
$product->metaRecords; // Collection
```

---

Caching
-------

[](#caching)

Caching is enabled by default and wraps every repository read with the configured cache store.

```
// config/meta.php
'cache' => [
    'enabled' => true,   // set false to disable entirely
    'store'   => null,   // null = default cache store; or e.g. 'redis'
    'ttl'     => 3600,   // seconds
    'prefix'  => 'meta',
],
```

Clear the cache manually:

```
php artisan meta:cache:clear
```

---

Events
------

[](#events)

Events are dispatched after every write (enabled by default, toggle with `'events' => false` in config).

EventWhen`Daikazu\LaravelMeta\Events\MetaCreated`A new key is written`Daikazu\LaravelMeta\Events\MetaUpdated`An existing key is overwritten`Daikazu\LaravelMeta\Events\MetaDeleted`A key is removed (`forget`, `flush`)All three carry the same constructor signature:

```
public function __construct(
    public Model  $model,
    public string $key,
    public mixed  $oldValue,
    public mixed  $newValue,
) {}
```

Example listener:

```
use Daikazu\LaravelMeta\Events\MetaUpdated;

Event::listen(MetaUpdated::class, function (MetaUpdated $event): void {
    logger("Meta key [{$event->key}] changed on {$event->model->getMorphClass()} #{$event->model->getKey()}");
});
```

---

Schemas
-------

[](#schemas)

Register a schema to attach casts and validation rules to specific keys. Schemas must be registered before the first `->meta` access on that model class (e.g. in a service provider or `AppServiceProvider::boot()`).

### Schema Field Classes

[](#schema-field-classes)

ClassCast applied`TextMeta``string``NumberMeta``float``ToggleMeta``boolean``DateMeta``datetime``JsonMeta``json`All field classes extend `MetaField` and share these fluent methods: `->required()`, `->min(int)`, `->max(int)`, `->default(mixed)`.

```
use Daikazu\LaravelMeta\Schema\NumberMeta;
use Daikazu\LaravelMeta\Schema\TextMeta;
use Daikazu\LaravelMeta\Schema\ToggleMeta;

// In AppServiceProvider::boot()
Product::metaSchema([
    TextMeta::make('seo.title')->required()->max(60),
    NumberMeta::make('weight')->min(0),
    ToggleMeta::make('featured'),
]);
```

### Validation

[](#validation)

Call `->validate()` with an array of key-value pairs to run the schema's Laravel validation rules:

```
// Throws Illuminate\Validation\ValidationException on failure
$validated = $product->meta->validate([
    'seo.title' => 'My Product Page',
    'weight'    => 1.5,
]);
```

Only keys present in both the payload and the schema are validated; unknown keys pass through unchecked.

---

Computed Meta
-------------

[](#computed-meta)

Computed meta keys are resolved lazily from a callback and are never persisted. Register before first `->meta` access.

```
// In AppServiceProvider::boot()
Product::computedMeta([
    'profit' => fn (Product $product): int => $product->price - $product->cost,
]);

// Usage
$product->meta->get('profit');   // int, evaluated fresh each time
$product->meta->has('profit');   // true
```

Setting a computed key throws `InvalidArgumentException`.

---

Indexed Metadata
----------------

[](#indexed-metadata)

For hot keys that need fast `WHERE` queries without JSON path overhead, configure them in `indexed_keys`. The package maintains a sidecar `model_meta_index` table with typed columns (`string_value`, `numeric_value`, `boolean_value`, `datetime_value`).

```
// config/meta.php
'indexed_keys' => ['featured', 'status'],
```

New writes to listed keys update the sidecar automatically. To backfill existing rows after adding a key to the list:

```
php artisan meta:index
```

> **Warning — backfill before relying on a newly indexed key.** Adding a key to `indexed_keys` for a table that **already has data** does not retroactively populate the sidecar. Until you run `php artisan meta:index`, `whereMeta()` on that key (which routes through the sidecar) will return **incomplete results**, silently omitting pre-existing rows. Always run the backfill immediately after adding a key.

> **Note — only `whereMeta()` uses the sidecar.** Equality/operator queries via `whereMeta()` (and `whereMetaNot`) read from the indexed sidecar table. `whereMetaIn`, `whereMetaBetween`, `whereMetaNull`, and `orderByMeta` always read the base `model_meta` table and are unaffected by indexing.

---

Artisan Commands
----------------

[](#artisan-commands)

CommandDescription`meta:install`Publish config + migrations, optionally run migrate`meta:install --force`Force-overwrite previously published files`meta:cache:clear`Flush the configured meta cache store`meta:index`Rebuild the indexed metadata sidecar from existing rows---

Testing
-------

[](#testing)

```
composer test
```

64 tests, Pest 4, PHPStan max (level 10).

---

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for recent changes.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Mike Wall](https://github.com/daikazu)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance90

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

48d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4039367?v=4)[Mike Wall](/maintainers/daikazu)[@daikazu](https://github.com/daikazu)

---

Top Contributors

[![daikazu](https://avatars.githubusercontent.com/u/4039367?v=4)](https://github.com/daikazu "daikazu (1 commits)")

---

Tags

laravellaravel metadaikazu

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/daikazu-laravel-meta/health.svg)

```
[![Health](https://phpackages.com/badges/daikazu-laravel-meta/health.svg)](https://phpackages.com/packages/daikazu-laravel-meta)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M48](/packages/spatie-laravel-pdf)[codewithdennis/filament-select-tree

The multi-level select field enables you to make single selections from a predefined list of options that are organized into multiple levels or depths.

330530.5k30](/packages/codewithdennis-filament-select-tree)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
