idci/simple-metadata-bundle
A small Symfony bundle for attaching simple metadata to your objects, letting you store extra key-value data alongside entities without schema changes. Lightweight and easy to wire into existing projects, though it has minimal adoption and may need review before production use.
The honest first step: idci/simple-metadata-bundle will not install or boot in a Laravel application. It is a Symfony 2 bundle — no ServiceProvider, no facade, no publishable config, and its composer.json pins Symfony 2.x components and a Doctrine ORM bridge that hard-conflict with PHP 8.x and any Laravel 9+. composer require idci/simple-metadata-bundle will either fail resolution or install code that never executes in Laravel's kernel. With 2 stars and 0 dependents, there is no community fork to fall back on either.
So "getting started" means one of two things, and you should decide within the first 30 minutes:
Entity/ and Service/ directories, and read the metadata model plus the get/set/remove service. The whole bundle is small enough to read in an hour. You are extracting the pattern, not the code.Where to look first when reading the source:
Metadata entity mapping — this tells you the storage model (side table vs. serialized column, polymorphic vs. per-entity). This is the architectural decision you actually care about.composer.json — confirm the PHP and Symfony constraints before investing any more time. If it requires symfony/*: ^2.x, stop and build natively.First use case to target when you reimplement: attaching per-tenant or per-integration attributes to a single domain model (e.g. a Product with partner-specific fields, an Order with fulfillment-provider metadata). Start there rather than attempting a general-purpose metadata layer across every model — the indexing and query needs differ wildly per model.
The 10-minute native baseline if you skip the source entirely: a JSON column with an Eloquent cast covers the majority of "attach arbitrary attributes" needs and is a fraction of the work:
// migration
$table->json('metadata')->nullable();
// model
protected $casts = ['metadata' => 'array'];
Reach for the polymorphic side table only when you need to query across metadata keys efficiently or attach metadata to many heterogeneous models.
Best when metadata is owned by one model, read by key, and queried rarely or coarsely.
class Product extends Model
{
protected $casts = ['metadata' => 'array'];
public function setMeta(string $key, mixed $value): static
{
$meta = $this->metadata ?? [];
$meta[$key] = $value;
$this->metadata = $meta;
return $this;
}
public function getMeta(string $key, mixed $default = null): mixed
{
return data_get($this->metadata, $key, $default);
}
}
This is roughly what the bundle's service layer gives you, without any of the framework mismatch. It is the correct default for a Laravel team.
Choose this only if the product needs "find all records where metadata x = y". Implement the bundle's model shape natively:
// metadata table: metable_type, metable_id, key, value, type, timestamps
// composite index on (metable_type, metable_id, key) — and on (key, value) if you filter by key
trait HasMetadata
{
public function metadata(): MorphMany
{
return $this->morphMany(Metadata::class, 'metable');
}
public function setMetadata(string $key, mixed $value): Metadata
{
return $this->metadata()->updateOrCreate(
['key' => $key],
['value' => $this->encodeValue($value), 'type' => get_debug_type($value)]
);
}
}
Note the updateOrCreate on ['key' => $key] — upsert semantics are the thing the bundle handles and the thing you must specify deliberately.
Regardless of storage, expose metadata through a small interface (MetadataStore) and bind it in a service provider. This is the pattern the Symfony bundle enforces structurally via its service layer, and it is the one piece worth copying verbatim in spirit. It means a JSON-column implementation today can become a side-table implementation tomorrow via a container binding, not a refactor.
type column exists for a reason — "1", 1, and true round-trip differently. Cast on write and on read; don't rely on the database to preserve type in a string column.upsert/updateOrCreate batches; per-key saves are an N+1 storm on save.->with('metadata') when listing models, exactly as you would with any morph relation.->cascadeOnDelete() to the morph foreign key or a model deleting observer; orphaned metadata rows are the classic failure mode of this pattern.--ignore-platform-reqs to force it in — dependency resolution may succeed, but Laravel's kernel never registers Symfony bundles, so you get dead code and a false sense of having adopted a library.Bundle class, a DI extension, YAML/XML service definitions, and Doctrine event hooks. None of that has a Laravel analogue. Even the "reusable" domain layer likely imports Symfony\Component\* and Doctrine mapping annotations throughout — check before assuming anything is salvageable.symfony/*: ^2.x. Under Laravel 10/11 (PHP 8.1/8.2) this is a hard conflict, and even if you port the source, PHP 8 deprecations (dynamic properties, implicit nullables, ${var} interpolation) will surface as fatal errors under Laravel's error handler.setMetadata('x', ...) when x already exists overwrite, throw, or append? The bundle's service defines this; your reimplementation must define it explicitly too, or you get intermittent data loss.composer.json.How can I help you explore Laravel packages today?