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

appstract/laravel-meta

Appstract Laravel Meta makes it easy to add, manage, and render SEO-friendly meta tags in Laravel apps. Define titles, descriptions, Open Graph and Twitter tags in PHP, then output them cleanly in your Blade views.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the package

composer require appstract/laravel-meta

The package targets the Laravel 5.5–5.8 era. On 5.5+ the Appstract\Meta\MetaServiceProvider is auto-discovered; on older versions register it manually in config/app.php.

2. Publish and run the migration

php artisan vendor:publish --provider="Appstract\Meta\MetaServiceProvider"
php artisan migrate

This creates a single polymorphic meta table (metable_type, metable_id, key, value, timestamps). There is no config file worth mentioning — the schema is the configuration.

3. Attach the trait to any model

use Appstract\Meta\HasMeta;

class Post extends Model
{
    use HasMeta;
}

4. First use case

The canonical day-one use case is "a handful of per-record flags/settings I don't want columns for":

$post->setMeta('reading_time', 4);

$post->getMeta('reading_time');           // "4"
$post->getMeta('missing', 'fallback');    // "fallback"
$post->hasMeta('reading_time');           // true
$post->removeMeta('reading_time');

Where to look first: open vendor/appstract/laravel-meta/src/HasMeta.php before writing anything real. The whole package is only a few hundred lines, and the public API is small — reading the trait takes five minutes and prevents 90% of the surprises below. Note the exact trait namespace on your installed tag (Appstract\Meta\HasMeta vs. a Traits\ sub-namespace) rather than trusting the README from 2018.

Implementation Patterns

Settings bag on a model. The most common pattern — a User or Team with a meta relationship acting as a sparse settings store:

$user->setMeta('timezone', 'Europe/Amsterdam');
$user->setMeta('digest_frequency', 'weekly');

Prefer this over json columns when different parts of the app (and different packages) own different keys and you want to read/write them independently.

Controller / form pattern. Map validated request fields to meta keys in a loop instead of one line per field:

$settings = $request->validate([
    'timezone'         => 'required|timezone',
    'digest_frequency' => 'in:daily,weekly,never',
])['settings'] ?? [];

foreach ($request->only(['timezone', 'digest_frequency']) as $key => $value) {
    $user->setMeta($key, $value);
}

Bulk updates belong in a transaction. There's no "set many" method, so wrap loops yourself:

DB::transaction(function () use ($user, $data) {
    foreach ($data as $key => $value) {
        $user->setMeta($key, $value);
    }
});

Reading everything at once. The meta() relation yields Meta models, so hydrate a plain array for views:

$meta = $post->meta->pluck('value', 'key')->toArray();

Eager-load it when rendering lists: Post::with('meta')->paginate().

Querying by metadata. Treat meta like a filter table and use whereHas:

Post::whereHas('meta', function ($q) {
    $q->where('key', 'featured')->where('value', '1');
})->get();

Keep this pattern out of hot paths — see the indexing note below.

Seeding / factories. Seed meta after the parent exists, since metable_id needs a real key. Add a HasMeta helper state or a small withMeta([...]) factory method in your test suite rather than hardcoding keys in feature tests.

Cast at the edges, not in the database. Store scalars as strings and convert in an accessor or a small value-object wrapper ($post->getMeta('published_at') → Carbon::parse(...)). The package does no casting, which is fine — just be consistent about it in one place.

Morph map early. If you attach the trait to several models, register Relation::morphMap() at boot so metable_type stores 'post' rather than the FQCN. Renaming a model later otherwise orphans its metadata silently.

Gotchas and Tips

It's unmaintained (last release Jan 2018). Before adopting, actually install it on your Laravel version and run the migration. Expect to adjust the migration (column lengths, index support) and to re-check the trait if you're on PHP 8+. Treat the package as a snippet library you can fork, not an actively maintained dependency.

No type casting — booleans are a trap. setMeta('is_active', false) stores an empty string; getMeta('is_active') returns "", not false. Normalize explicitly:

$post->setMeta('is_active', $flag ? '1' : '0');
filter_var($post->getMeta('is_active'), FILTER_VALIDATE_BOOLEAN);

Arrays/objects need json_encode on write and json_decode on read — always paired, or you'll get strings where you expect structures.

getMeta returns null for missing keys, so $post->getMeta('x') ?? 'default' is your friend. Be careful with the default argument form — don't assume a missing key and a key deliberately set to '0' are distinguishable unless you check hasMeta().

Check the value column width in the published migration. If it's a string(255) on your tag, long text (bio, notes, markdown) will either truncate or throw depending on MySQL strict mode. Change it to text in the migration before migrate in production.

N+1 is the default. Serializing a collection of models with $model->meta triggers a query each. Always with('meta') on index endpoints. If you only need one key in a list view, select it in a withCount/whereHas instead of loading the whole collection.

Add an index. The stock migration likely doesn't index (metable_type, metable_id, key). Add a composite index (and consider making it unique if you want to guarantee one row per key). Without it, whereHas filters on meta become full scans once the table grows.

Orphaned rows. Deleting a model does not necessarily remove its meta rows unless cascading is configured. Add cleanup — a deleted model event, a scheduled prune, or a proper FK with onDelete('cascade') if your schema allows polymorphic-agnostic cleanup (usually a job, not an FK).

Watch for duplicates. If setMeta is implemented as create-then-update (typical), concurrent requests can race and leave two rows for the same key. Wrap writes in a transaction and/or add the unique index above; on read, decide whether pluck('value','key') (last wins) or strict first-match is your contract.

Class renames break everything. Because metable_type stores the class name by default, renaming Post → Article silently detaches all metadata. Register a morph map on day one, and if you must rename, write a one-off migration that rewrites metable_type.

Debugging. When a value "doesn't save," go straight to the table: DB::table('meta')->where('metable_id', $post->id)->get(). Nine times out of ten it's a mismatched key name, a truncated value, or a stored "" that looks falsy in a Blade conditional.

Extension points are thin. You can subclass/replace the Meta model or add your own casting layer around the trait, but there's no config for table name, key column, or casting. If you need typed casts, per-key validation, or schemaless JSON, evaluate spatie/laravel-schemaless-attributes or a dedicated settings package before committing — for a greenfield project in 2024+, that trade-off is usually worth making.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core