kunstmaan/pagepart-bundle
Kunstmaan PagePartBundle is a Symfony bundle for building pages from reusable page parts. It offers configurable content blocks, admin management, and flexible templating for modular CMS pages.
kunstmaan/pagepart-bundle is a Symfony 2-era component of the Kunstmaan CMS. It does not give you a CMS on its own — it gives you the page/block composition layer: a page is a collection of PagePartRef rows pointing at small Doctrine entities ("pageparts"), each rendered by its own Twig view. The last tagged release was 2014, so read everything below as Symfony 2.3–2.8, PHP 5.4+, Doctrine annotations.
Minimal setup
composer require kunstmaan/pagepart-bundle
// app/AppKernel.php
$bundles = array(
// ...
new Kunstmaan\PagePartBundle\KunstmaanPagePartBundle(),
);
app/console doctrine:schema:update --force # creates page_part_ref + your pagepart tables
Where to look first (in the vendor directory, roughly in this order):
Helper/PagePartInterface.php and Helper/HasPagePartsInterface.php — the whole contract in two files.Entity/AbstractPagePart.php — the mapped superclass you extend for every new block.Entity/PagePartRef.php — the join object that actually owns page → pagepart, the context (region) and the sequencenr (ordering).PagePartAdmin/PagePartAdminConfigurator.php — how the admin UI is told which pageparts a page template accepts.Twig/Extension/PagePartTwigExtension.php and Resources/views/ — how a pagepart becomes HTML.DependencyInjection/Configuration.php — the exact config key (typically kunstmaan_page_part:) and its options.First use case: a text block on the homepage
// src/Acme/SiteBundle/Entity/TextPagePart.php
namespace Acme\SiteBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Kunstmaan\PagePartBundle\Entity\AbstractPagePart;
/**
* @ORM\Entity
* @ORM\Table(name="acme_text_page_parts")
*/
class TextPagePart extends AbstractPagePart
{
/** @ORM\Column(type="text", nullable=true) */
private $body;
public function getBody() { return $this->body; }
public function setBody($body) { $this->body = $body; return $this; }
public function getDefaultView()
{
return 'AcmeSiteBundle:PagePart/TextPagePart:view.html.twig';
}
public function getDefaultAdminType()
{
return new \Acme\SiteBundle\Form\TextPagePartAdminType();
}
}
{# src/Acme/SiteBundle/Resources/views/PagePart/TextPagePart/view.html.twig #}
<div class="text-pagepart">{{ pagepart.body|raw }}</div>
{# page template: iterate the pageparts for a region/context #}
{% for pagepart in page.pageparts %}
{{ render_pagepart(pagepart) }}
{% endfor %}
The Twig function name and whether you pass a view explicitly are the two things that changed most across versions — open Twig/Extension/PagePartTwigExtension.php and read the getFunctions() block before writing your template; that twenty-line method will save you an hour of guessing.
One page template class per layout. Page templates implement PageTemplateInterface and return one PagePartAdminConfigurator per region. That configurator is the single source of truth for "what can a webmaster drop into the header?" — it is consumed by the admin, not by the frontend. Keep the context names (main, header, footer, sidebar) identical between configurator and template; they are string keys, not constants.
public function getPagePartAdminConfigurations()
{
$main = new PagePartAdminConfigurator();
$main->setContext('main');
$main->addPagePart('text', 'Acme\SiteBundle\Entity\TextPagePart');
$main->addPagePart('image', 'Acme\SiteBundle\Entity\ImagePagePart');
$footer = new PagePartAdminConfigurator();
$footer->setContext('footer');
$footer->addPagePart('text', 'Acme\SiteBundle\Entity\TextPagePart');
return array($main, $footer);
}
PagePartRef is the real model. A pagepart entity has no idea which page it is on. Attaching it means creating a ref:
$ref = new PagePartRef();
$ref->setPage($pageTranslation); // refs hang off the page translation, not the page
$ref->setPagePart($textPagePart);
$ref->setContext('main');
$ref->setSequencenr(3);
$em->persist($ref);
$em->flush();
Because the ref is separate, the same TextPagePart instance can legitimately appear on several pages — useful for shared disclaimers, dangerous for anything the editor expects to be private to one page. Decide this deliberately; most projects should create a fresh entity per placement.
Separate data from presentation, literally. Give each pagepart a view path in getDefaultView() and treat the Twig file as the only place HTML lives. Business logic (link resolution, image URL generation, filtering) belongs in the entity getter or a Twig extension — the pagepart view should be dumb enough that a frontend developer can rewrite it without touching PHP.
Reuse the admin form types, don't build parallel UI. getDefaultAdminType() returns a Symfony form type; extend the bundle's abstract admin type so field ordering, tabs and the "save/delete" chrome stay consistent. Custom admin templates go under Resources/views/PagePartAdmin/, not in your app's generic form theme.
Rendering outside the page template. For sitemaps, RSS, search indexing or emails, don't duplicate the loop — resolve the pageparts via the page's accessor and render through the bundle's renderer service (the same one the Twig extension delegates to). This keeps escaping and view resolution identical between web and CLI output.
Extension point checklist when a pagepart doesn't fit: new context in the configurator → new form type → new Twig view → new entity. In that order. If you find yourself editing PagePartRef or the admin controller, you are almost certainly solving the wrong problem.
composer update it casually, and budget a fork if you need fixes. The high opportunity score mostly reflects how much room there is for a modern replacement, not how ready it is.page_part_ref row pointing at nothing, and the template loop explodes (or silently renders nothing, which is worse). Delete the ref, or configure cascade/orphanRemoval on the page side and verify with a query: SELECT COUNT(*) FROM page_part_ref r LEFT JOIN ... WHERE ... IS NULL.'main' vs 'Main' vs ' content' yields an empty region with no error. Add a smoke test that asserts each configured context renders at least one known pagepart for a fixture page.sequencenr is an integer maintained by the admin drag-and-drop. Bulk operations (imports, content migrations) must set it explicitly, or you get pageparts in unpredictable primary-key order.page.pageparts and touching pagepart.body will lazy-load one query per ref per part. Use a fetch-join on the ref repository for listing pages; it is the single biggest performance lever in this bundle.app/Resources/KunstmaanPagePartBundle/views/ override bundle views without touching vendor. Use it for one-off tweaks instead of subclassing entities.doctrine:schema:update in dev is fine, but generate a real migration before deploy, and remember the page_part_ref table itself must already exist.page_part table with STI, AbstractPagePart must stay a mapped superclass — adding @Entity to it, or a second inheritance level, produces duplicate-column errors that are annoying to unpick.page_part_ref with the right context → the context is in the page template's configurator → getDefaultView() resolves to a file that exists → the Twig function is being called at all. That's ~90% of issues.How can I help you explore Laravel packages today?