kaliop/content-decorator-bundle
A Symfony bundle from Kaliop that adds a decoration layer for content objects, letting you attach custom presentation methods and computed values without changing the underlying content model. Helps keep templates clean by moving view-specific logic into reusable decorators.
This bundle sits between Ibexa's raw content API and your application code. Instead of passing Content / ContentInfo / field collections around, you get typed model objects and a repository that returns them. Day to day, that means less instanceof and getFieldValue() plumbing in controllers and more ordinary PHP objects you can type-hint, test and serialize.
1. Pick the right version line first. The platform constraints are hard, not advisory:
| Line | Ibexa | Symfony | PHP |
|---|---|---|---|
| 2.x | 5.0 | 7.4 LTS | 8.3+ |
| 1.x | 4.6 | 5.4 LTS | 8.1+ |
Install with composer require kaliop/content-decorator-bundle, then register the bundle in config/bundles.php. If you are on Ibexa 4.6 / Symfony 5.4, pin the 1.x line explicitly — main targets the 2.x stack.
2. Read the source in this order (the README is thin, the code is the documentation):
src/contracts — the stable API surface. This is what you should type-hint against. Start here.src/bundle — the DI extension and Configuration class. This tells you the actual config keys, service IDs and how decorators get registered.src/lib — mappers, factories, repositories, events. Useful for understanding behaviour, but treat as implementation detail.Use bin/console debug:container --tag=<bundle tag> and debug:container <RepositoryInterface> to find the repository service ID rather than guessing.
3. First use case: decorate one content type. Do not migrate everything at once. Pick a single content type (say article), define one typed model for it, wire it through the bundle config, and have exactly one controller or query path consume it. Verify the mapping in a functional test before touching a second content type.
Model-per-content-type, immutable. Each decorated model should be a small, readonly-ish object exposing typed properties or getters for the fields you actually use. Resist the urge to mirror every field — the mapping layer is the cost you are paying, so map what the domain needs. Keep Ibexa value objects (e.g. ContentType, relations) out of your public model surface where practical, or at least behind contracts.
Repository as the single entry point. The value of this package collapses if half your code uses the decorated repository and half uses the raw Ibexa content service. Establish a rule: anything that needs a domain object goes through the decorator repository; anything that needs raw content (admin screens, low-level sync jobs) is explicitly marked as bypassing it. Mixing the two produces objects that look alike but behave differently.
Wire, don't subclass. Registration happens through bundle configuration, not by extending package internals. Your application owns its model classes; the bundle owns the mapping. When you need new behaviour in the package itself, the contribution path is defined: add it in src/lib, wire it in src/bundle, and only touch src/contracts if the public API genuinely must grow.
Keep controllers thin. The typical controller becomes: inject repository → query by identifier/location/type → receive typed model → hand to a responder or template. Business logic that used to be field extraction moves into model methods or a domain service, which is the point of the abstraction.
Use events for cross-cutting post-processing. The bundle ships events from src/lib. Prefer a listener that augments or validates a mapped object over duplicating mapping logic in every caller — and prefer it over forking the mapper.
Test at two levels. Unit-test your model classes with plain PHPUnit (no kernel). For the mapping, write functional tests that boot the kernel and assert on real fixtures — mapping bugs are almost always about field edge cases (empty fields, missing translations, multi-value attributes), which unit tests with mocks will not catch. Where the mapper can be instantiated directly, test it standalone to keep the suite fast.
Multi-site / multi-version awareness. If you run several siteaccesses or content type variants, keep decoration config explicit per context rather than relying on defaults inherited from the bundle's Configuration. Explicit config survives upgrades; implicit defaults do not.
The version coupling is the biggest operational risk. A major line is locked to a specific Ibexa/Symfony/PHP triple. Upgrading Ibexa will likely force an upgrade of this bundle, and the AGENTS notes explicitly say maintainers will not widen constraints without approval. Plan the bundle upgrade as part of your platform upgrade, not after it.
Contracts are stable; lib is not. Build your integration against src/contracts. SemVer forbids BC breaks in minor/patch releases, but the layering exists precisely so that implementation classes in src/lib can be reshaped. If you find yourself type-hinting a concrete mapper or repository implementation from src/lib, you have created upgrade debt.
Adoption is minimal — 2 stars, 0 dependents. Treat this as a single-maintainer dependency. Practical mitigations: pin exact versions, keep an application-side interface in front of the repository so you can swap the implementation, read the mapper code you depend on before upgrading, and be prepared to fork. It is MIT licensed, so forking is a viable fallback rather than a last resort.
Config quirks to check on day one. Symfony's config component will reject unknown keys, but silently ignored or defaulted values are common in bundle extensions. After wiring, dump the resolved container parameters and confirm your decorator registrations actually landed. Also remember to clear the container cache after config changes — a stale compiled container is the most common "my decorator isn't applied" cause.
Missing and empty fields. Decide early whether an absent field yields null, a default object, or an exception, and confirm which the mapper actually does. Content objects in the wild frequently have unset fields, empty rich text, or untranslated attributes; a mapper that assumes presence will fail on production content, not on your fixtures.
Mapping cost. Decoration is not free — every query through the repository pays for object construction. Avoid decorating content types you only ever display as a title and link, and watch for N+1 behaviour when models pull in relations or locations lazily. If a listing endpoint gets slow, the fix is usually to select less, not to optimise the mapper.
Don't bypass and re-enter. Fetching raw content and manually constructing a model "just this once" is how the invariant breaks. If you need a decorated object in a context that has raw content, add a path through the repository rather than hand-assembling.
If you contribute back, run the required checks before opening a PR: composer validate --strict, composer check-cs, composer phpstan, composer deptrac. Deptrac enforces the bundle/contracts/lib split, so a layering violation will fail CI rather than review. Target main for features and the relevant maintenance branch (e.g. 1.x) for fixes to older lines, and update README.md plus release notes whenever behaviour changes.
How can I help you explore Laravel packages today?