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 Metadata Laravel Package

jobmetric/laravel-metadata

A Laravel package for attaching flexible key-value metadata to Eloquent models. It offers polymorphic metadata storage, simple trait-based integration, and helpers to set, get, update, and query metadata without changing your existing database schema.

View on GitHub
Deep Wiki
Context7

Getting Started

The package adds a polymorphic key/value metadata store to any Eloquent model. The minimal path looks like this:

  1. Install and publish

    composer require jobmetric/laravel-metadata
    php artisan vendor:publish --provider="Jobmetric\Metadata\MetadataServiceProvider"
    php artisan migrate
    

    You should now have a metadata table (morphs + key + value columns). Check the published migration first — that's the single best place to understand what the package actually stores.

  2. Look at the source before trusting it. At 11 stars and a 0.115 score, this is a low-traffic package. Open src/ first: read the trait (usually HasMetadata), the Metadata model, and the config file. Ten minutes here will save you guessing at method names later.

  3. Attach the trait to a model

    use Jobmetric\Metadata\HasMetadata;
    
    class Product extends Model
    {
        use HasMetadata;
    }
    
  4. First use case — the canonical one is a flexible sidecar for admin-editable fields that don't warrant a column: SEO meta title/description, per-product display flags, feature toggles, third-party IDs, or localized marketing copy. If a value is queried/filtered constantly or needs a DB constraint, use a real column; metadata is for "extra fields that admins add without a migration."

  5. Try one round-trip before building anything on it:

    $product->setMetadata('seo_title', 'Red Shoe');
    $product->getMetadata('seo_title'); // verify the actual method name in the trait
    

Implementation Patterns

Wrap it in your own service/repository. Don't scatter setMetadata() calls across controllers. Create a small ProductMetadata class that knows the whitelist of allowed keys, casts types, and applies defaults:

class ProductMetadata
{
    public const KEYS = ['seo_title', 'seo_description', 'badge'];

    public function __construct(private Product $product) {}

    public function all(): array
    {
        return collect(self::KEYS)
            ->mapWithKeys(fn ($k) => [$k => $this->product->getMetadata($k)])
            ->all();
    }

    public function sync(array $data): void
    {
        foreach (array_intersect_key($data, array_flip(self::KEYS)) as $key => $value) {
            $this->product->setMetadata($key, $value);
        }
    }
}

Form + validation pattern. Treat metadata like any other form field: validate with Rule::in(ProductMetadata::KEYS) then loop the whitelist in the request handler. This keeps "unknown key" injections and typos out of your table.

Typed values. Metadata tables are strings. Either pick a convention (JSON-encode all values, decode on read) or use the package's type support if it has one. Consistency matters more than which choice you make — mixed scalar-vs-JSON values in the same column is a future debugging session.

Bulk reads for indexes. If you render a product list with metadata, don't N+1. Eager-load the metadata relationship once, then read keys off the collection — or add a scoped "load all metadata for these IDs" query in your repository.

Admin CRUD builder. The natural fit is a generic admin screen: list Product::with('metadata'), show a key/value editor grid, and persist with a diff (add/update/delete). This is where metadata shines because you don't ship a migration for every new field marketing wants.

Settings / app-wide config. Because the relationship is polymorphic, you can attach metadata to a singleton "Settings" model or an App\Settings row to get a lightweight key/value settings store — but do not use this for values that need type safety or auditing. Prefer config/ or a dedicated settings package for that.

Migration / sync helper. Keep a metadata:backfill artisan command that reads canonical keys from your code and writes defaults onto existing records. When you add a new metadata key, you can run it once to seed nulls/booleans so consumers don't have to handle missing keys.

Cache with a tag. Metadata is a per-model read on nearly every page. Cache the whole metadata array per model ID, invalidate on save/delete in your service layer. Use Cache::tags(['metadata', "product:{$id}"]) if you're on Redis/Memcached.

Gotchas and Tips

  • Verify the API in src/ before writing tutorials. This package's method names and return shapes are not documented at any scale. Grep the trait for public function and read the Metadata model's casts/fillable — that's your real contract.
  • Polymorphic key collisions. The morphs relationship means metadata is scoped to (model_type, model_id) + key. Two models of the same class can't share a key row, but if the package doesn't have a unique index on (metadataable_type, metadataable_id, key) you can get duplicates. Add the unique index in a follow-up migration if it's missing.
  • Orphaned rows. Deleting a model doesn't cascade-delete metadata unless the FK is set up. Add $model->metadata()->delete() in an observer's deleting hook, or a Model::deleted listener — otherwise your table grows silently.
  • Bulk operations. Looping setMetadata() per key in a request means N writes. Wrap the whole sync in DB::transaction() and consider an upsert() on the metadata table for large payloads.
  • JSON casts cut both ways. If the package casts value to JSON, setMetadata('x', 'plain string') may store "plain string" and read it back fine — but manually inspecting the DB in a query tool becomes confusing. Document the convention.
  • Reading missing keys. Default to a predictable null/default, don't throw. getMetadata('x') ?? config('product.defaults.x') is the pattern; if the package throws on missing keys, wrap your own accessor.
  • Querying. Filtering "products where badge = 'sale'" through the metadata table is slow and awkward (join + key filter). If a metadata key becomes a filter on the listing page, promote it to a real column. This is the most common reason teams regret metadata tables.
  • Config file. Publish and read config/metadata.php before going live — the table name, morph key names, and any type/length limits live there. Changing the table name post-migration means editing the migration and the config together.
  • Testing. Use a SQLite in-memory DB in your tests, but be aware morphs() and unique indexes behave slightly differently from MySQL. Run at least one integration test against your real DB engine.
  • Upgrade caution. Last release was Sept 2025 with very low adoption — pin a version in composer.json (e.g. "jobmetric/laravel-metadata": "~1.0") rather than *, and skim CHANGELOG/diffs on bumps.
  • Escape hatch. Because it's MIT and small, forking or vendoring is cheap. If you hit a shape-of-the-API limitation (typed values, searchable metadata, translations), a fork with a better cast or index is usually faster than fighting the package.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource