Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Json Meta Laravel Package

hyvor/laravel-json-meta

hyvor/laravel-json-meta adds JSON metadata columns to Eloquent models via a simple trait, with helpers for reading, writing, and querying arbitrary meta fields stored in JSON columns.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect before you write code. The package is archived (last release May 2024), so treat the source and its test suite as the real documentation — clone it, read src/ and tests/, and confirm the trait name, attribute/config syntax and supported cast types against the version you pin. Do not assume the README is complete.

composer require hyvor/laravel-json-meta

Minimum viable setup:

  1. Add a JSON column in a migration:
Schema::table('users', function (Blueprint $table) {
    $table->json('meta')->nullable();
});
  1. Add the package trait to the model and declare the shape of the JSON column in PHP. The package's contract is "strict meta": you enumerate the allowed keys and their types on the model, and it validates/casts on read and write rather than letting arbitrary JSON through.
class User extends Model
{
    use HasMeta; // confirm exact trait name in src/

    // Declare allowed meta keys + types (see README/tests for exact syntax)
    protected array $meta = [
        'notifications' => 'bool',
        'timezone'      => 'string',
        'onboarded_at'  => 'datetime',
    ];
}
  1. Read and write through the model, not the raw column:
$user->meta->timezone = 'Europe/Amsterdam';
$user->meta->notifications = true;
$user->save();

Where to look first, in order: tests/ (behavioural truth) → the trait source (what it hooks into: casts, getAttribute, setAttribute, save) → the migration/stub if one is published → README last.

First real use case: per-user or per-tenant settings — notification prefs, UI state, feature flags, onboarding flags — where you'd otherwise be writing a migration for every new boolean. That's the sweet spot: a fixed, code-reviewed set of keys, changing values frequently, changing shape rarely.

Implementation Patterns

Treat the meta schema as code, not data. Adding a key means a PR, a review and a deploy — that is the point of the package. If a stakeholder wants arbitrary user-defined keys, this is the wrong tool; use a separate settings table with a key/value pair or a schemaless JSON column.

Defaults belong in the declaration, not in null checks scattered across views. Give every key a default in the model declaration so $user->meta->notifications is never null for a newly created user. This removes an entire class of ?? false noise from Blade and API resources.

Centralise writes behind intention-revealing methods. Don't let controllers poke individual keys from five places:

public function markOnboarded(): void
{
    $this->meta->onboarded_at = now();
    $this->save();
}

public function updatePreferences(array $prefs): void
{
    $this->meta->notifications = (bool) ($prefs['notifications'] ?? $this->meta->notifications);
    $this->meta->timezone = $prefs['timezone'] ?? $this->meta->timezone;
    $this->save();
}

Validate at the boundary, with rules that mirror the declaration. The package's strictness protects the storage layer (wrong types are rejected on save), but it will not produce friendly 422s. Put explicit rules in the FormRequest, and keep them in sync with the meta declaration — a single rules() method on the model that both the FormRequest and the declaration reference is worth the extra indirection.

Use it alongside casts, not instead of them. A datetime meta key should behave like a Carbon instance in your API resource and in diffForHumans(). Verify the package actually returns Carbon and not a string — if it doesn't, wrap it in an accessor rather than casting at every call site.

Query with the database's JSON functions, not with PHP. The package handles hydration; filtering is still your job:

User::whereJsonContains('meta->notifications', true)->get();
User::where('meta->timezone', 'Europe/Amsterdam')->get();

If you find yourself needing more than a handful of these, promote the hot key to a real indexed column. JSON filtering on MySQL is a full scan of the document and does not use an index unless you add a generated column.

Factories and seeders. Because meta is strict, a factory that sets meta to an arbitrary array will either silently drop keys or throw. Seed through the model's public methods, or declare a withMeta() factory state that goes through the same path production code uses.

Migrating an existing JSON column. Write a one-shot artisan command that reads every row's blob, maps known keys onto the new declaration, logs and drops unknown keys, and writes back. Run it in a transaction per chunk and keep the old column until you've verified counts per key.

Because it's archived, budget for a fork. Vendor it under packages/ with a path repository, or fork to your org and point composer.json at the fork. That costs you nothing today and means a Laravel 12/13 compatibility fix is a one-line patch instead of a rewrite under deadline.

Gotchas and Tips

Archived means no upstream fixes — pin exactly. Use a fixed version constraint ("hyvor/laravel-json-meta": "1.0.0", not ^1.0). The risk isn't the package breaking; it's that a future Laravel upgrade changes getAttribute/castAttribute internals and you discover it in production. Add a test that round-trips every declared meta key so a framework bump fails loudly.

Missing key vs. null key are different things. Decide and document which one $user->meta->timezone returns when the column is null for a legacy row. Most bugs here surface only for pre-existing records, not for records created after install.

Dirty tracking on JSON is coarse. Assigning one key often marks the whole column dirty and writes the full document back. That's usually fine, but it means two concurrent requests touching different keys will last-write-wins each other. If that matters, wrap the read-modify-write in a transaction with lockForUpdate(), or move the contended key to its own column.

Database differences bite. MySQL stores JSON as a binary format that normalises key order and number formatting; Postgres jsonb reorders keys too; older SQLite has JSON1 but different operator support. Assert on decoded values in tests, never on raw column strings.

$model->meta shadowing. If the model already has a meta() relation, an accessor, or a meta attribute from a previous implementation, the trait will collide. Check for existing meta usages with grep -rn "->meta" app/ before installing.

Mass assignment is a trap. If the meta object is fillable, $model->update($request->all()) can write keys you never declared. Keep meta out of $fillable and assign through explicit methods.

Test the failure mode. Write one test that assigns a wrong type and asserts the exception type — that test is your proof the "strict" guarantee actually holds, and it's the first thing that will break on a Laravel upgrade.

Extension point. If the package's built-in types don't cover a case (money, enums), don't fight it — register a custom cast in Laravel and declare the meta key with that cast, or wrap the value in a small value object at the model boundary. Keep the package doing storage and validation only.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin