opifer/content-bundle
Opifer ContentBundle is a Symfony CMS bundle for managing pages and reusable content blocks. It provides structured content types, template integration, and admin tooling to help teams build and maintain content-driven PHP applications.
This is not a Laravel package. It's a Symfony 2.x/3.0-era bundle (last subtree split 2016-01-13, MIT, archived) that ships the domain model and rendering for a page/block CMS. The admin UI lives in the sibling opifer/cms-bundle; this split gives you Content (a page with slug + alias), Template (a pointer to a Twig view), Block (a reusable content chunk) and ContentBlock (the ordered join between them).
Minimal path:
composer require opifer/content-bundle only resolves inside a Symfony ^2.7/^3.0 app. If it fails, run composer why-not opifer/content-bundle — the answer is usually symfony/symfony or doctrine/orm. There is no Laravel integration route here without building your own bridge for Doctrine + Form + Twig; practically, don't.app/AppKernel.php (new Opifer\ContentBundle\OpiferContentBundle()) and import its routing (resource: "@OpiferContentBundle/Resources/config/routing.yml") under whatever prefix you want.bin/console config:dump-reference opifer_content — before writing any YAML. The tree carries the class parameters (content_class, block_class, template_class, etc.) that you must point at your own entities.AppBundle\Entity\Content extends Opifer\ContentBundle\Model\Content), map them with Doctrine annotations, and wire the FQCNs into opifer_content. Then doctrine:schema:update --force.Where to look first, in order:
Model/Content.php, Model/Block.php, Model/Template.php — the actual data contract.ContentManager.php / BlockManager.php — the two services (opifer.content.content_manager, opifer.content.block_manager) you'll call from your own code.Controller/ContentController.php — the reference rendering pipeline to copy or override.Resources/config/services.xml and DependencyInjection/Configuration.php — the only source of truth for service IDs and config keys.First useful thing to build: seed one Template row (name + Twig view path), create a Content with slug home, and render it from your own controller:
$content = $this->get('opifer.content.content_manager')
->getRepository()
->findOneBy(['slug' => $slug, 'active' => true]);
return $this->render($content->getTemplate()->getView(), ['content' => $content]);
That's the whole loop: slug → active content → template view → blocks.
Entity inheritance is mandatory, not optional. The bundle's models are mapped superclasses; you cannot use them directly. Create Content, Block, Template, ContentBlock in your app, map them, and register the class names in opifer_content. If the bundle exposes resolve_target_entities, point those at your classes too so associations (e.g. a page's template) resolve correctly.
Query through the manager, not the entity manager. ContentManager centralizes repository creation and any slug/alias lookups. Calling $em->getRepository(Content::class) directly works but bypasses whatever filters the manager applies, and it's the first thing to break if you later swap the class.
Model a block type as a trio. Each block kind is three artifacts:
TextBlock extends Block) holding the payload (value, media, url, …);FormType that edits those fields — this is what the admin uses to configure the block;Shared fields (title, name, active) live on Block; never re-declare them in the subclass. Register block types the way the bundle's compiler pass expects (check DependencyInjection/Compiler/ for the tag name) — an unregistered block type silently never appears in the editor.
Compose pages through ContentBlock, not a plain collection. Ordering is a position integer on the join entity. To append a block:
$content->addBlock($block); // creates the ContentBlock join
$em->persist($content);
$em->flush();
Reordering means renumbering the position values yourself and flushing once; there is no ordering service.
Prefer copying the controller over fighting it. Subclass ContentController, override the action, and register your route before the bundle's. You keep the bundle's 404/inactive handling and gain full control over variables passed to the view.
Seed templates via fixtures. Template rows are database config that point at Twig file names. Committing a fixture that creates them (with explicit view strings) is the only way to keep a multi-environment install in sync, since the admin UI is in a different package.
No autowiring. services.xml is explicit Symfony 2 style. Consume the managers by ID ($this->get('opifer.content.content_manager')) or inject them by ID in your own service definition. Don't expect __construct(ContentManager $m) to resolve on its own.
Cache deliberately. Nothing purges HTTP/proxy caches when content changes. Either key the cache on content + block IDs, or add a Doctrine postUpdate/postPersist listener that clears the page's cache entry. This is the single biggest pattern omission in the bundle.
repositories entry (type: vcs) plus a replace for the original name; don't expect upstream merges.*_class config value is missing or points at the abstract model instead of your concrete entity. Confirm with bin/console doctrine:mapping:info.Media entity) use @ORM\AssociationOverride, not a new property.view string on a Template row surfaces as a Twig LoaderError at render time, not at save time. Validate the view exists in your fixture/admin save path.alias for secondary URLs, add a uniqueness check in your own code.|raw. If editors aren't fully trusted, sanitize in the block's Twig partial or before persist.position values render in database order, which looks random after a few edits. Renumber on every save if you build your own editor.bin/console debug:container opifer.content lists the actual services (the docs and README are stale), bin/console debug:router | grep -i content shows what routing you really imported, and config:dump-reference opifer_content is the only accurate config schema.setDefinition or decorate rather than editing the bundle's XML — a composer update (if the repo is still reachable) would overwrite it.Content, render each Template's view with a dummy content variable. The bundle's own test coverage is thin, and a failing template only shows up on the exact page that uses it.How can I help you explore Laravel packages today?