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 Eloquent Metable Laravel Package

elhareth/laravel-eloquent-metable

elhareth/laravel-eloquent-metable adds flexible key-value metadata to any Eloquent model via a simple trait, letting you attach custom attributes without extra migrations or schema changes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require elhareth/laravel-eloquent-metable
php artisan vendor:publish --provider="Elhareth\Metable\MetableServiceProvider"
php artisan migrate

Publish both the config and the migrations in one go — the migration is the piece people forget, and you'll get confusing "no such table: meta" errors on first save otherwise. Check config/metable.php first; it holds the table name and (as of v1.0.0) the key type used for meta rows.

Attach the trait

use Elhareth\Metable\HasMeta;

class Product extends Model
{
    use HasMeta;
}

First use case: ship a small, schema-free attribute bag on a model whose shape isn't known yet — per-tenant feature flags, CMS page settings, product spec sheets, onboarding answers. The moment you find yourself writing add_column_xyz for a field only 5% of rows use, that's the signal to reach for this package.

The v1.0.0 release is the first stable tag, so treat it as the baseline. The most notable functional addition is ULID support for meta records, which matters if the rest of your schema is already ULID/UUID-based.

Implementation Patterns

Method names below follow the package's documented conventions — confirm the exact signatures in the README before committing, especially if you're porting from another metable package.

Single-value read/write

$product->setMeta('weight_kg', 1.4);
$product->getMeta('weight_kg');            // 1.4
$product->getMeta('missing', 'fallback');  // 'fallback'

Use this in form request handlers and admin panels where the field set is dynamic. Pair it with a $fillable-style allowlist of meta keys so you're not writing arbitrary user input straight into the bag.

Bulk sync (the pattern you'll actually use most)

$product->syncMeta([
    'weight_kg'  => 1.4,
    'colour'     => 'slate',
    'hazmat'     => false,
]);

syncMeta in a single call is what you want inside a DB::transaction() — it avoids per-key round trips and makes partial writes less likely. Never loop setMeta() inside a request without a transaction.

Reading efficiently

$products = Product::with('meta')->paginate(25);

The meta relation is a polymorphic has-many. Eager-load it anywhere you render meta in a list; without it you get one query per row per key. For index pages, consider selecting only the keys you need:

Product::with(['meta' => fn ($q) => $q->whereIn('key', ['colour', 'weight_kg'])])

Querying by meta

Use the provided scope rather than hand-rolling joins:

Product::whereMeta('colour', 'slate')->get();

Anything more complex (numeric ranges, LIKE, ordering by a meta value) is where this approach starts to hurt — the values column is typically a string/text column, so comparisons are lexicographic unless you cast. For genuine filtering requirements, promote the field to a real column and keep meta for display-only data.

API and resource layers

class ProductResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'     => $this->id,
            'meta'   => $this->meta->pluck('value', 'key'),
        ];
    }
}

Flattening to key => value is friendlier for the frontend than the raw collection of meta models, and it lets you apply casts in one place.

ULID meta records (new in v1.0.0)

If your app uses ULIDs, set the key type in config before running migrations, then re-publish. Meta models will need $keyType = 'string' and $incrementing = false if you've extended the base meta model. Do this on a fresh install or a dedicated migration — not mid-flight on a populated table.

Custom meta model

If you need extra columns on meta rows (audit trail, source, ordering), extend the package's meta model, point the config at it, and add the columns in your own migration. This is the cleanest extension point and avoids forking.

Gotchas and Tips

  • 0.x → 1.0.0 upgrade: first stable tag, so assume the public API settled here. Diff the README against your installed version, run the full test suite, and specifically check any method you call that touches the meta key/type — ULID support implies changes to how keys are generated. Pin the version in composer.json until you've verified.
  • Values are strings at rest. Store what you mean and cast on read. boolean false and "false" are indistinguishable after a round trip unless you normalise. For structured data, json_encode/json_decode explicitly rather than hoping the package handles arrays.
  • The migration's value column type is the usual culprit for silent truncation on long text. If you're storing JSON blobs or long descriptions, widen it in your own migration.
  • No unique constraint by default in most metable schemas means duplicate keys are possible if you write outside syncMeta. Add a unique index on (model_type, model_id, key) in a follow-up migration if integrity matters — and be aware it changes setMeta semantics from "append" to "replace".
  • Eager-load or suffer. This is the single biggest performance trap. A 50-row index page without with('meta') is 51 queries.
  • Migrations are published, not loaded from the package. That means package upgrades won't update your schema. After upgrading, diff the package's stub migrations against yours — the ULID support in v1.0.0 is exactly the kind of change that lands in a stub.
  • $model->meta is a collection, not an array. $model->meta['colour'] won't do what you expect. Use getMeta() or ->meta->pluck('value', 'key').
  • Debugging tips: log $model->meta()->toSql() and bindings when a whereMeta scope returns nothing — the polymorphic type column (model_type) must match the fully-qualified class name exactly, and a renamed class or a model with a custom morph map will silently break lookups.
  • Testing: use RefreshDatabase and assert on getMeta() rather than raw table rows, so a future schema change (ULID keys, widened columns) doesn't break your suite for the wrong reason. Add at least one test that asserts a null/missing key returns the default, since that's where behaviour differs between metable packages.
  • Don't let meta become a shadow schema. If you're writing whereMeta chains three deep with casts and orderBy, you've built a database in the wrong place — add the column.
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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle