Views
HTML in Concept Core is rendered via Twig or Plates.
Templates are .twig or .php files in the views directory; in a controller the response is built with
ViewResponseFactoryInterface::create() (see Responses → HTML).
Low-level rendering — ViewInterface::render() (services, error handler).
View paths are configured in config/view.php and via
ComponentInterface::viewPaths(), viewContexts(), and
viewExtensions() on component classes.
Twig namespaces
A template from a controller is specified as @namespace/path/without_extension.
Namespaces are registered in configuration:
// config/view.php
'paths' => [
'frontend' => '/resources/views/frontend',
'dashboard' => '/resources/views/dashboard',
],
// AuthAdminComponent::viewPaths()
public function viewPaths(): array
{
return [
'auth-admin' => $this->pathManager->toRelative($this->componentDir . '/Views'),
];
}
File src/Components/AuthAdmin/Views/users/list.twig → @auth-admin/users/list.
The resources/views directory (PathName::VIEWS in paths.php) is also added
as a fallback without a namespace — rarely needed if all templates live in namespaced paths.
Rendering from a controller
return $this->viewResponse->create('@frontend/index');
return $this->viewResponse->create('@cabinet/index', [
'user' => $user,
'latestProject' => $latestProject,
]);
The second argument — variables for this request only. They are merged with the global context
(errors, old, flashes, csrf_token) from
ShareViewDataMiddleware.
Layouts and partials
Typical structure:
resources/views/frontend/
├── layouts/base.twig
├── partials/_navbar.twig
├── partials/_flash.twig
└── index.twig
src/Components/UserCabinet/Views/
├── layouts/cabinet.twig
├── layouts/auth.twig
└── index.twig
{# index.twig #}
{% extends '@frontend/layouts/base.twig' %}
{% block title %}Головна{% endblock %}
{% block body %}
<h1>Контент</h1>
{% endblock %}
{# Підключення фрагмента #}
{% include '@frontend/partials/_flash.twig' %}
Component pages often extend a shared layout from another namespace
(e.g. admin: @auth-admin/users/list.twig → @dashboard/layouts/base.twig).
URLs in templates
In skeleton, uri(), url(), and base_url() are registered by
AppExtension
(config/view.php → view.extensions).
<a href="{{ uri('architecture') }}">Architecture</a>
<a href="{{ url('cabinet.login') }}">Login</a>
<a href="{{ uri('admin.user.edit', { id: user.id }) }}">Edit</a>
<link rel="canonical" href="{{ base_url() }}">
uri()— relative path by route name (UrlGeneratorInterface::uri())url()— full URL (UrlGeneratorInterface::url())base_url()— application base (UrlGeneratorInterface::base())
More on route names — routing.
Global variables and flash
| Variable | Source |
|---|---|
errors | Validation errors (flash) |
old | Previous input after failed validation |
flashes | All flash messages (success, error…) |
csrf_token | Token for forms |
route_namespace | UI context by URI prefix — details |
<input name="email" value="{{ old.email|default('') }}"
class="input {{ errors.email is defined and errors.email ? 'is-invalid' : '' }}">
{% if errors.email is defined and errors.email is not empty %}
<div class="field-error" role="alert">
{{ errors.email is iterable ? errors.email|join(', ') : errors.email }}
</div>
{% endif %}
Flash messages:
{% if flashes is defined and flashes|length > 0 %}
{% for type, messages in flashes %}
{% for message in messages %}
<div class="alert alert-{{ type == 'error' ? 'danger' : type }}">{{ message }}</div>
{% endfor %}
{% endfor %}
{% endif %}
More on validation — Requests and validation.
route_namespace
route_namespace is not the same as a Twig template namespace
(@cabinet/index). It is the logical UI context: based on the current URL,
the framework determines which “skin” the page uses (public site, admin, cabinet).
The variable is available in all templates as a global: it is added by
ContextExtension via ViewContextResolver.
Why it exists
A single application can have multiple zones with different layouts and styles. For 404/500 or shared partials, you need to know which set of views to include without checking the path in every Twig file.
route_namespace answers: URI prefix → context name from configuration.
How it is determined
- In
view.contexts(application) andComponentInterface::viewContexts()(components), define a map of URI prefix → context. -
ViewContextResolvertakes the request path and iterates prefixes from longest (so/admin/usersmatches/admin, not/). ContextExtension::getGlobals()returnsroute_namespaceto Twig.
// config/view.php
'contexts' => [
'/' => 'frontend',
],
// AuthAdminComponent::viewContexts()
public function viewContexts(): array
{
return [
'/admin' => 'dashboard',
];
}
| URL | route_namespace |
|---|---|
/, /architecture | frontend |
/admin/users | dashboard |
/cabinet/profile | cabinet |
Example: error pages
In skeleton, a single dispatcher in resources/views/errors/ includes 404/500
for the appropriate context:
{# resources/views/errors/404.twig #}
{% include '@' ~ (route_namespace|default('frontend')) ~ '/errors/404.twig' %}
/unknown-page→frontend→@frontend/errors/404.twig(public layout)/admin/missing→dashboard→@dashboard/errors/404.twig(admin layout)
Public 404 extends @frontend/layouts/base.twig;
admin 404 extends @dashboard/layouts/error.twig.
Without route_namespace, you would have to duplicate conditions like “if /admin — one template, else another”.
Other uses in Twig
{# Різний partial залежно від зони #}
{% if route_namespace == 'dashboard' %}
{% include '@dashboard/partials/_sidebar.twig' %}
{% else %}
{% include '@frontend/partials/_navbar.twig' %}
{% endif %}
{# Динамічний include (ignore missing — якщо partial відсутній) #}
{% include '@' ~ route_namespace ~ '/partials/_breadcrumbs.twig' ignore missing %}
Important nuance
Values in contexts (dashboard, frontend, cabinet)
must match the Twig namespace where the required templates actually live.
In AuthAdmin the context is named dashboard, not auth-admin: error pages
and the shared admin layout live under @dashboard/, while CRUD content is under @auth-admin/.
For /cabinet the context is cabinet, but UserCabinet has no separate
Views/errors/. If you need a custom 404 in the cabinet —
add @cabinet/errors/404.twig or change the dispatcher to fall back to
frontend.
Configuration config/view.php
return [
'view' => [
'extensions' => [
AppExtension::class, // has_component()
ContextExtension::class, // route_namespace global
UrlExtension::class, // uri(), url(), base_url()
],
'paths' => [ /* namespace => path from project root */ ],
'contexts' => [
'/' => 'frontend', // URI prefix => context name for route_namespace
],
'cache_dir' => 'views', // storage/cache/views in production
],
];
- extensions —
Twig\Extension\AbstractExtensionclasses, resolved from the DI container - paths — Twig namespaces for the application
- contexts — URI prefix →
route_namespace(details) - cache_dir — subdirectory under
storage/cache; whenapp.debugis set, Twig cache is disabled
Views in components
In AuthAdminComponent, the viewPaths(), viewContexts(), and
viewExtensions() methods declare Twig namespaces, URI contexts, and extensions.
During ComponentsServiceProvider::boot(), data
is added to registries and picked up by Twig on HTTP requests (not in CLI).
// AuthAdminComponent — Twig extension + templates
public function viewExtensions(): array
{
return [TwigExtension::class]; // auth()
}
public function viewPaths(): array
{
return [
'auth-admin' => $this->pathManager->toRelative($this->componentDir . '/Views'),
];
}
public function viewContexts(): array
{
return [
'/admin' => 'dashboard',
];
}
In Twig: {{ auth().check() }} (if the component extension is loaded),
{{ has_component('UserCabinet') }} — from AppExtension.
Custom Twig extension
class MyExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('format_money', [$this, 'formatMoney']),
];
}
public function getFilters(): array
{
return [
new TwigFilter('excerpt', [$this, 'excerpt']),
];
}
}
Register the class in view.extensions (application) or the component’s
viewExtensions() method. In templates: {{ price|format_money }}, {{ format_money(price) }}.
Direct call to ViewInterface
/** @var ViewInterface $view */
$view = $container->get(ViewInterface::class);
$html = $view->render('@frontend/errors/404', ['exception' => $e]);
The .twig extension is added automatically if not specified.
Telemetry: TelemetryEvent::TPL_RENDERED in TwigView::render()
(Telemetry).
In debug mode — DebugExtension and Twig Profiler.
Error pages
The dispatcher resources/views/errors/{code}.twig via
route_namespace includes
@frontend/errors/... or @dashboard/errors/....
In production the error handler renders the same path; if the file is missing — fallback to 500.
Additional PHP fallback files — resources/views/errors/fallback/
(PathName::ERRORS_FALLBACK_VIEWS).
Static assets in templates
CSS/JS usually live in public/ and are linked with absolute paths
(/assets/css/frontend.css, /vendor/bootstrap/...).
Components can publish assets via ComponentInterface::assets()
(copied to public/components/...).
Keep logic (database queries, authorization) in controllers and services. In Twig — display, conditions, loops, and helper filters.