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.phpview.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

VariableSource
errorsValidation errors (flash)
oldPrevious input after failed validation
flashesAll flash messages (success, error…)
csrf_tokenToken for forms
route_namespaceUI 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

  1. In view.contexts (application) and ComponentInterface::viewContexts() (components), define a map of URI prefix → context.
  2. ViewContextResolver takes the request path and iterates prefixes from longest (so /admin/users matches /admin, not /).
  3. ContextExtension::getGlobals() returns route_namespace to Twig.
// config/view.php
'contexts' => [
    '/' => 'frontend',
],

// AuthAdminComponent::viewContexts()
public function viewContexts(): array
{
    return [
        '/admin' => 'dashboard',
    ];
}
URLroute_namespace
/, /architecturefrontend
/admin/usersdashboard
/cabinet/profilecabinet

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-pagefrontend@frontend/errors/404.twig (public layout)
  • /admin/missingdashboard@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
    ],
];
  • extensionsTwig\Extension\AbstractExtension classes, resolved from the DI container
  • paths — Twig namespaces for the application
  • contexts — URI prefix → route_namespace (details)
  • cache_dir — subdirectory under storage/cache; when app.debug is 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.