Installation
composer require berry/symfony
Add the bundle to config/bundles.php:
return [
Berry\Symfony\BerrySymfonyBundle::class => ['all' => true],
];
First Use Case: Rendering a Page
Create a controller to render a simple page using the AppLayout from the README:
// src/Controller/HomeController.php
use App\View\AppLayout;
use App\View\IndexPage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class HomeController extends AbstractController
{
public function index(AppLayout $layout, IndexPage $indexPage): Response
{
$content = $indexPage->render();
$html = $layout->render('My App', $content);
return new Response($html->render());
}
}
Key Files to Explore
src/View/ – Custom view components (e.g., AppLayout.php, IndexPage.php).config/packages/berry_symfony.yaml – Bundle configuration (if any).templates/ – Legacy Twig templates (if migrating from Twig to Berry).Use dependency injection to compose views hierarchically:
// src/View/IndexPage.php
class IndexPage
{
use WithGenerateUrlLocator; // For Symfony URL generation
public function render(): Element
{
return div()
->class('content')
->text('Welcome to Berry!');
}
}
AppLayout into controllers or other views to nest content.Leverage Symfony services (e.g., Router, Twig, AssetMapper) via traits:
use Berry\Symfony\Locator\Trait\WithAssetMapperLocator;
use Berry\Symfony\Locator\Trait\WithRouterLocator;
class AppLayout
{
use WithAssetMapperLocator, WithRouterLocator;
public function render(string $title, Element $content): Element
{
return html()
->child(head()
->child(title()->text($title))
->child(link()
->rel(Rel::Stylesheet)
->href($this->assetMapper->getUrl('css/pico.min.css'))));
}
}
Use Berry’s eDSL for dynamic HTML:
// src/View/UserProfile.php
class UserProfile
{
public function render(string $username, array $posts): Element
{
return div()
->class('profile')
->child(h1()->text("Hello, $username"))
->child(ul()->children(
array_map(fn($post) => li()->text($post), $posts)
));
}
}
Embed HTMX or JavaScript dynamically:
// In AppLayout::render()
->child(script()
->src($this->assetMapper->getUrl('js/htmx.min.js'))
->attr('integrity', '...')
->attr('crossorigin', 'anonymous'))
Generate URLs in views:
// In IndexPage::render()
->child(a()
->href($this->generateUrl('app_home'))
->text('Home'))
No Twig Compatibility Layer
Asset Pipeline Quirks
asset() helper (from Symfony’s AssetMapper) may not work out-of-the-box with Berry’s Element structure.WithAssetMapperLocator trait and call $this->assetMapper->getUrl().Caching Headaches
Element objects are immutable. Overusing them in loops can bloat memory.->children() for dynamic lists.Symfony 6+ Deprecations
AssetMapper changes in Symfony 6.3+).(new Element())->render() to debug raw HTML output.->attr('data-debug', 'true') to elements to inspect attributes.dd($element->render()) to dump HTML in controllers.Custom Elements
Extend Berry\Element for domain-specific tags:
class CardElement extends Element
{
public function header(string $text): self
{
return $this->child('header')->text($text);
}
}
Middleware for Berry
Add a Kernel event listener to modify responses:
// src/EventListener/BerryResponseListener.php
use Symfony\Component\HttpKernel\Event\ResponseEvent;
class BerryResponseListener
{
public function onKernelResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
if ($response->headers->get('Content-Type') === 'text/html') {
$response->setContent(
(string) new AppLayout()->render('Modified', div()->text('Hello!')))
);
}
}
}
Testing Views
Use PHPUnit to test Element rendering:
public function testLayoutRendersCorrectly()
{
$layout = new AppLayout();
$html = $layout->render('Test', div()->text('Content'));
$this->assertStringContainsString('<title>Test</title>', $html->render());
}
->children() instead of deeply nested ->child() calls).DOMContentLoaded.How can I help you explore Laravel packages today?