View
The View module integrates the Twig template engine into NeoPHP. It exposes the ViewManager for rendering templates, an extension system allowing Twig functions and filters to be added from any module, and a controller extension that injects the render() and template() methods directly into controllers.
Module Structure
View/
├── ViewManager.php # Main Twig manager
├── ViewModule.php # Registration in the DI container
├── Interface/
│ └── TwigExtensionInterface.php # Contract for Twig extensions
├── Extension/
│ └── ViewControllerExtension.php # Injects render() and template() into controllers
└── Exception/
└── ViewException.php # Exception specific to rendering errors
Twig Configuration
Configuration is read from two files: twig.config.php and app.config.php.
src/MyProject/Config/twig.config.php:
return [
'cache' => false, // true in production
'debug' => true, // Adds the DebugExtension and {{ dump() }}
'auto_reload' => true, // Recompiles modified templates
'auto_escape' => 'html', // Automatic HTML escaping
'charset' => 'UTF-8',
'strict_variables' => false, // true = exception if variable unknown
'options' => [], // Additional options passed to Twig\Environment
];
src/MyProject/Config/app.config.php:
return [
'date' => [
'timezone' => 'Europe/Paris', // Twig and PHP timezone
],
'general' => [
'name' => 'My Application',
'version' => '1.0.0',
],
];
The timezone is applied both to PHP (date_default_timezone_set) and to Twig (CoreExtension::setTimezone).
When cache is true, compiled templates are stored in Storage/var/cache/Twig/.
ViewManager
Neo\Core\View\ViewManager wraps the Twig\Environment instance and provides the rendering methods.
Rendering Methods
use Neo\Core\View\ViewManager;
$view = $container->get(ViewManager::class);
// Render with an exception if the template does not exist
$html = $view->render('articles/list.twig', [
'articles' => $articles,
'title' => 'All articles',
]);
// Silent render: returns null if the template cannot be found
$html = $view->renderIfExists('partials/sidebar.twig', ['user' => $user]);
if ($html !== null) {
// display the sidebar
}
// Direct access to the Twig\Environment instance
$twig = $view->getTwig();
Adding a Twig Extension
$view->addExtension(new MyExtension());
addExtension() iterates over the functions and filters returned by the extension and registers them in Twig via TwigFunction and TwigFilter. Each entry can be either a direct callable, or an array ['callable' => ..., 'options' => [...]].
Controller Extension: render() and template()
Neo\Core\View\Extension\ViewControllerExtension is a controller extension (annotated #[Extension(type: ExtensionTypeEnum::CONTROLLER)]) that automatically injects two methods into all controllers.
render()
Renders a Twig template and directly returns a Response object with the Content-Type: text/html; charset=UTF-8 header.
use Neo\Core\Controller\AbstractController;
class ArticleController extends AbstractController
{
public function list(): Response
{
$articles = $this->get(ArticleRepository::class)->findAll();
return $this->render('articles/list.twig', [
'articles' => $articles,
]);
}
public function detail(int $id): Response
{
$article = $this->get(ArticleRepository::class)->findById($id);
return $this->render('articles/detail.twig', [
'article' => $article,
'title' => $article->title,
]);
}
}
template()
Renders a Twig template and returns the HTML content as a string (without creating a Response). Useful for including a fragment in a more complex response or for rendering partial components.
class EmailController extends AbstractController
{
public function send(): Response
{
$emailContent = $this->template('emails/welcome.twig', [
'name' => 'Alice',
]);
$this->get(Mailer::class)->send(
to: 'alice@example.com',
subject: 'Welcome',
body: $emailContent
);
return $this->json(['message' => 'Email sent.']);
}
}
The app Global Variable in Templates
ViewControllerExtension enriches the app global variable available in all templates by automatically adding the current Session and Cookie:
{# Access session data in Twig #}
{% if app.session.get('user_id') %}
Logged in as: {{ app.session.get('user_name') }}
{% endif %}
{# Access the application name (from general config) #}
<title>{{ app.name }}</title>
{# Access cookies #}
{% if app.cookie.has('lang') %}
Language: {{ app.cookie.get('lang') }}
{% endif %}
TwigExtensionInterface
Neo\Core\View\Interface\TwigExtensionInterface is the contract that every Twig extension must implement.
interface TwigExtensionInterface
{
/**
* @return array<string, mixed>
*/
public function getFunctions(): array;
/**
* @return array<string, mixed>
*/
public function getFilters(): array;
}
Each method returns an array indexed by the function/filter name in Twig. The value can be:
Simple format (direct callable):
'myFunction' => fn(string $param): string => strtoupper($param),
Extended format (with Twig options):
'myFunction' => [
'callable' => fn(string $param): string => strtoupper($param),
'options' => ['is_safe' => ['html']], // TwigFunction/TwigFilter options
],
Creating a Twig Extension
To expose custom functions or filters in templates, simply create a class implementing TwigExtensionInterface and annotate it with #[Extension(type: ExtensionTypeEnum::VIEW)] so it is automatically detected and registered.
<?php
declare(strict_types=1);
namespace Neo\Src\MyProject\Extension;
use Neo\Core\Extension\Attribute\Extension;
use Neo\Core\Extension\Enum\ExtensionTypeEnum;
use Neo\Core\View\Interface\TwigExtensionInterface;
#[Extension(type: ExtensionTypeEnum::VIEW)]
class MyTwigExtension implements TwigExtensionInterface
{
public function getFunctions(): array
{
return [
// Simple function
'format_price' => fn(float $amount, string $currency = 'EUR'): string
=> number_format($amount, 2, ',', ' ') . ' ' . $currency,
// Function with Twig options (is_safe: html to avoid re-escaping)
'badge' => [
'callable' => fn(string $label, string $color = 'blue'): string
=> "<span class=\"badge badge-{$color}\">{$label}</span>",
'options' => ['is_safe' => ['html']],
],
// Access to an injected service
'article_count' => fn(): int => $this->articleRepository->count(),
];
}
public function getFilters(): array
{
return [
'initials' => fn(string $name): string => implode('', array_map(
fn(string $word): string => strtoupper($word[0]),
explode(' ', $name)
)),
'truncate' => [
'callable' => fn(string $text, int $length = 100): string
=> mb_strlen($text) > $length
? mb_substr($text, 0, $length) . '...'
: $text,
'options' => [],
],
];
}
}
Usage in Templates:
{# Functions #}
{{ format_price(article.price) }}
{{ format_price(article.price, 'USD') }}
{{ badge('New', 'green') }}
{{ article_count() }} articles available
{# Filters #}
{{ user.name|initials }}
{{ article.description|truncate(150) }}
Twig Globals
The app variable is available globally in all templates. It is built from the general section of app.config.php.
// app.config.php
return [
'general' => [
'name' => 'MySite',
'version' => '2.1.0',
'maintenance' => false,
'support' => 'support@mysite.com',
],
];
{# In any template #}
<title>{{ app.name }}</title>
<meta name="version" content="{{ app.version }}">
{% if app.maintenance %}
<div class="alert">Site under maintenance.</div>
{% endif %}
{# Enriched by ViewControllerExtension during a controller's render() #}
Hello {{ app.session.get('user_name') ?? 'visitor' }}
Error Handling
ViewManager converts Twig exceptions into ViewException with appropriate HTTP codes:
| Twig Exception | Code | Title |
|---|---|---|
Twig\Error\LoaderError | 404 | Template Not Found |
Twig\Error\SyntaxError | 500 | Template Syntax Error |
Twig\Error\RuntimeError | 500 | Template Runtime Error |
renderIfExists() silently intercepts LoaderError and returns null, without throwing an exception.
// In a service, handle template errors
try {
$html = $view->render('my-template.twig', $data);
} catch (ViewException $e) {
// $e->getCode() returns 404 if the template cannot be found
// $e->getMessage() contains the detail of the Twig error
logger()->error($e->getMessage());
$html = '<p>Rendering error.</p>';
}
ViewModule
Neo\Core\View\ViewModule registers ViewManager in the DI container with ConfigModule as a dependency.
// Automatic registration by the framework
// The module declares its dependency on ConfigModule
class ViewModule implements ModuleInterface
{
public function dependencies(): array
{
return [ConfigModule::class];
}
public function register(Container $container): void
{
$container->set(ViewManager::class, fn(Container $c) => new ViewManager($c));
}
public function init(Container $container): object
{
return $container->get(ViewManager::class);
}
}
ViewManager is instantiated only once (singleton in the container) and shared across all controllers and services that need it.