zoha/laravel-meta
zoha/laravel-meta lets Laravel apps attach and manage SEO metadata for Eloquent models and pages. Define titles, descriptions, Open Graph and Twitter tags, store custom meta polymorphically, and render clean meta tags in Blade via a simple, configurable API. Ideal for blogs, CMS pages, and product c...
Architecture fit. zoha/laravel-meta is a small Eloquent extension: a trait you add to a model, backed by a polymorphic meta table storing key/value pairs (with a type hint for casting). It is an EAV-lite pattern, and that framing should drive the decision. It is a good fit for sparse, open-ended, low-cardinality attributes that belong to an entity but not to the core schema — CMS page settings, per-tenant feature toggles, product spec blobs, user preferences, integration-specific config. It is a poor fit for anything you intend to filter, sort, aggregate, or join on at scale, for attributes with referential integrity requirements, or for values that participate in business-critical workflows. If a field is queried in a list view, it belongs in a real column.
Integration feasibility. Cheap. Composer install, publish and run a migration, use HasMeta on the model, then $model->saveMeta('key', $value) / getMeta('key'). There is no service provider ceremony, no config sprawl, and no meaningful learning curve — a mid-level Laravel developer is productive in under a day. The package is MIT and self-contained, so the exposure is bounded: worst case you vendor the trait and the migration into your own codebase, which is a few hundred lines.
Technical risk. The dominant risk is maintenance, not code. 236 stars, a single-maintainer project, and a last release in March 2024 means the package is in a "probably fine, definitely not guaranteed" tier. Laravel's annual major releases break traits and query builders regularly, and a package this thin is exactly the kind that silently stops receiving compatibility patches. Secondary risks: the EAV read path (N+1 unless the trait exposes eager loading and your team uses it), type coercion surprises when values round-trip through the database as strings, and the absence of a foreign key on the polymorphic relation, which means orphaned meta rows when models are hard-deleted unless you handle it. The low "opportunity" score in the source data reinforces that this is a solved-problem space with stronger alternatives (spatie/laravel-schemaless-attributes for JSON-column storage, or a hand-rolled JSON column) — so the package needs to earn its place rather than be a default.
Key questions to answer before committing.
Stack fit. This is a Laravel-only, Eloquent-native package and assumes a relational database. It fits cleanly into a conventional Laravel monolith or modular monolith where models already carry traits. It fits poorly in a codebase that has standardized on JSON columns for extensibility, or one with strict read-model/repository separation, because the trait mutates and persists through the model.
Migration path. If we already store ad-hoc attributes in a JSON column, do not rip that out. Run the package alongside, migrate one attribute family at a time, dual-write for a release cycle, verify reads match, then cut over and drop the old column in a later deploy. If we are greenfield, adopt directly but only for models that pass the "never queried" test above. If we are currently storing meta in a hand-rolled table, the package is a lateral move — the honest comparison is "does it save us maintaining our own trait," and the answer is usually yes if the existing trait is under ~200 lines.
Compatibility. Pin the package to a known-good minor version and add it to the dependency-review list tied to Laravel upgrades. Verify database support for the migration across every engine we run (MySQL, Postgres, SQLite in test) before rollout. Confirm the package does not pull abandoned transitive dependencies. Require a passing test suite in our own repo that exercises meta read/write/cast/delete on a representative model — that test suite is our insurance against upstream abandonment, not the package's own CI.
Sequencing.
MetaStore / a small service or repository) in front of the trait so no domain code calls getMeta/saveMeta directly. This is the single highest-leverage decision here — it caps migration cost if we later swap to JSON columns or a different package, and it gives us one place to add caching and validation.Maintenance. Low ongoing code maintenance, moderate dependency maintenance. Budget roughly 1–2 engineer-days per year for Laravel upgrade compatibility checks, and hold a standing "vendor-in if abandoned" decision on file. The internal wrapper from step 2 is what converts this from a dependency risk into a contained, swappable implementation detail — without it, package churn becomes a codebase-wide refactor.
Support. No SLA, no commercial backing, community issues only. Treat the package as unsupported software: we are the first line of support. Any production incident traced to meta reads or writes will be debugged by our own team. That is an acceptable trade for a 200-line utility, not for anything on the critical path.
Scaling. The meta table grows as a multiple of the parent tables — a model with 20 attributes generates 20 rows. At tens of millions of parent rows this becomes a real table, and the composite index on (metable_type, metable_id, key) is mandatory, not optional. Read amplification is the practical ceiling: rendering 50 models with 10 meta fields each is 500 rows across either one wide join or 50 queries. Enforce eager loading in code review, and put a request-scoped or Redis cache in front of hot read paths. Write volume is modest but bursty — bulk imports that write meta per row will need chunked upserts rather than the per-call trait API.
Failure modes to design against.
Ramp-up. Very low. An experienced Laravel developer is productive in half a day; the team cost is not learning the API but learning the discipline — when not to use meta. Put the "never query meta in a list view" rule in the onboarding doc alongside the wrapper's interface, and make the wrapper the only sanctioned entry point so the constraint is enforced by tooling rather than convention.
How can I help you explore Laravel packages today?