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

Metadata Laravel Package

enhavo/metadata

Metadata component for the Enhavo content framework, providing models and services for storing, resolving, and rendering resource metadata such as titles, descriptions, and SEO-related attributes across pages.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you write a line of code, confirm what you're actually installing. enhavo/metadata is a [READ-ONLY] subtree split of the Enhavo CMS monorepo — a Symfony bundle, not a Laravel package. There is no service provider, no config/metadata.php, no facade. Your first job is a 30-minute audit:

composer show enhavo/metadata --all   # requirements, versions, release dates

Then open the repo and read, in this order:

  1. composer.json — the PHP floor, the symfony/* and doctrine/* constraints. This file decides everything. If it requires symfony/framework-bundle or doctrine/orm, you are not consuming this in Laravel; you are reading it for design.
  2. src/ — the Model/ or Entity/ directory. Look for a MetadataInterface, a concrete Metadata class, and whatever holds the key/value pairs (array, ArrayCollection, JSON string).
  3. Any Resources/config/doctrine/*.orm.xml or Entity/*.php with #[ORM\...] attributes — this tells you whether the package owns its table schema.
  4. The Form/ and DependencyInjection/ directories — these are where Symfony coupling lives and where portability dies.

First use case (Enhavo/Symfony): you have a content entity (a Page, an Article) and you want editors to set an SEO title and description in the admin without touching templates. You attach the package's metadata object to the resource, register the metadata form on the resource's admin form, and render it in Twig via the provided helper. You write no persistence code — the bundle's mapping handles it.

First use case (Laravel): you don't install it. You read MetadataInterface and the concrete model, note the field set (title, description, and whatever SEO fields exist), and reimplement that vocabulary in your own app in an afternoon. See the next section.

Where to look first if you go the Enhavo route: the monorepo, not this repo. github.com/enhavo/enhavo — the split mirrors it, but issues, PRs, and changelogs live upstream. Pin the split version to your Enhavo distribution version explicitly.

Implementation Patterns

Pattern 1 — Port the contract, not the code (default for Laravel)

This is what most Laravel teams should do. The package's value is its shape, not its bytes: metadata is a separate object attached to a resource, not a pile of columns on the resource table.

// app/Support/Metadata/Contracts/HasMetadata.php
interface HasMetadata
{
    public function metadata(): MetadataInterface;
}

Storage choice, in order of preference:

// A. JSON column — simplest, no joins, no extra table
Schema::table('pages', fn (Blueprint $t) => $t->json('metadata')->nullable());

// B. Polymorphic table — better when metadata has its own lifecycle,
//    per-key indexing, or multiple metadata "sets" per resource
Schema::create('metadata', function (Blueprint $t) {
    $t->id();
    $t->morphs('metadatable');
    $t->string('meta_key');
    $t->text('meta_value')->nullable();
    $t->timestamps();
    $t->unique(['metadatable_type', 'metadatable_id', 'meta_key']);
});

Prefer A unless you have a concrete query requirement on a specific key. JSON columns in MySQL 8 / Postgres are indexable via generated columns, and you avoid N+1 entirely because the metadata ships with the row.

Wrap it in a value object so callers never touch the raw array:

final class Metadata implements MetadataInterface
{
    public function __construct(
        private ?string $title = null,
        private ?string $description = null,
        private ?string $ogImage = null,
        private bool $noindex = false,
        private array $extra = [],
    ) {}

    public static function fromArray(array $data): self { /* hydrate */ }
    public function toArray(): array { /* serialize */ }
}

Cast it on the model — this is the whole trick, and it's the one thing Enhavo's form/admin integration buys you that you must rebuild deliberately:

protected function casts(): array
{
    return ['metadata' => MetadataCast::class]; // implements CastsAttributes
}

Pattern 2 — Defaults and inheritance on read, not on write

The most common real-world requirement is "fall back to the page title if no SEO title is set." Do not persist the fallback. Resolve at render time:

final class MetadataResolver
{
    public function resolve(Page $page): ResolvedMetadata
    {
        $meta = $page->metadata;

        return new ResolvedMetadata(
            title: $meta->title ?: $page->title,
            description: $meta->description ?: Str::limit(strip_tags($page->body), 155),
            canonical: $meta->canonical ?: route('pages.show', $page),
            noindex: $meta->noindex,
        );
    }
}

This keeps the stored metadata honest (what the editor actually typed) and makes the fallback chain a single testable unit instead of scattered ?? operators in Blade.

Pattern 3 — The blade component is the public surface

Whatever the storage, expose one entry point. Everything else is internal.

{{-- resources/views/components/seo.blade.php --}}
@props(['meta'])
<title>{{ $meta->title }}</title>
<meta name="description" content="{{ $meta->description }}">
@if($meta->noindex)<meta name="robots" content="noindex">@endif
<link rel="canonical" href="{{ $meta->canonical }}">
<meta property="og:title" content="{{ $meta->title }}">
<meta property="og:image" content="{{ $meta->ogImage }}">

Now swapping implementations later — or adopting a maintained Laravel SEO package — is a change to one component and one resolver, not a grep across fifty templates.

Pattern 4 — Form request + admin editing

The Enhavo package's selling point is "editable in admin without custom plumbing." The Laravel analogue:

class MetadataRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'metadata.title'       => ['nullable', 'string', 'max:70'],
            'metadata.description' => ['nullable', 'string', 'max:160'],
            'metadata.og_image'    => ['nullable', 'url'],
            'metadata.noindex'     => ['boolean'],
        ];
    }
}

The max:70 / max:160 limits are the actual product requirement — SEO titles truncate around 60–70 chars, descriptions around 155–160. Enforce them in validation, not in the editor's head.

Pattern 5 — If you are on Enhavo/Symfony

Consume through the Enhavo distribution. Practically:

  • Register the metadata form on your resource's admin form in the resource config.
  • Attach the metadata object to your entity per the package's model conventions.
  • Render via the bundle's Twig helper rather than hand-rolling <meta> tags — you get the package's field set and escaping for free.
  • Never composer require enhavo/metadata as a point dependency alongside a separate Enhavo install; version skew between the split and the distribution is the classic failure mode.

Gotchas and Tips

It is not a Laravel package, and no amount of wiring makes it one. There is no MetadataServiceProvider, no config/, no facade. Symfony components in a Laravel app will eventually collide with Laravel's own pinned Symfony versions during a framework upgrade — usually at the worst possible time. If coupling is high (Doctrine mappings, compiler passes, event subscribers), stop and reimplement.

The repo is a read-only split — you cannot patch in place. Any local change is a permanent divergence from upstream. A fork is a full ownership transfer: bug fixes, PHP version bumps, security review, forever. Budget for it or don't fork.

Zero stars, zero dependents, no independent docs. Every integration question is answered by reading source. This is fine for a few hundred lines of value objects; it is not fine for anything with meaningful runtime behavior. Treat the absence of external issue history as "no known failure modes mined," not "no failure modes."

Check the release cadence against your PHP version. The split's tags should track the Enhavo release line, but splits go stale silently. If the last release predates PHP 8.2, watch for dynamic-property deprecations; PHP 8.4 brings implicit-nullable deprecations. Also check whether the split is even load-bearing for Enhavo core — if it's an orphaned helper, maintenance is nobody's job.

Serialization is the sharp edge. If metadata round-trips through unserialize(), that's an object-injection surface the moment any of it is user-influenced. Read the storage/retrieval path before trusting it with editor input. Prefer JSON everywhere you control the code.

Schema drift is silent. If the package owns table definitions (Doctrine mappings or migrations), an upstream change can desync your database from your code with no changelog. If you adopt the code, pin the exact version and have a task to re-read the mapping diff on every composer update. If you adopt the pattern, you avoid this entirely.

N+1 on listing pages. Metadata attached per-record is the classic trap: a list of 50 resources becomes 51 queries. In Enhavo, check whether the metadata association is eagerly joined. In Laravel with a JSON column, the cast loads with the row and the problem disappears — a strong argument for Pattern 1 over a polymorphic table.

Don't persist computed defaults. Store what the editor typed; resolve fallbacks at read time. Otherwise changing your fallback rule requires a data migration across every row.

Validate lengths in the request, not the model. max:70 for titles, max:160 for descriptions. Model-level validation fires too late to guide the editor.

Keep one public entry point. A single Blade component plus a single resolver means swapping implementations later is a two-file change. If <meta name="description"> appears in eight templates, you've coupled yourself to the decision permanently.

If adopting in Enhavo, version-pin against the distribution. "enhavo/metadata": "x.y.*" matching your Enhavo release, and revisit on every framework upgrade. Do not let a subtree split drift underneath you.

The exit ramp is cheap if you design for it. Because the concept (metadata object attached to a resource) is framework-agnostic, a Laravel native implementation behind your own MetadataInterface costs roughly 1–2 weeks including tests — and leaves you owning it. That's the number to weigh against adopting a zero-star Symfony component into a Laravel codebase.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator