Представлення

HTML у Concept Core рендериться через Twig або Plates. Шаблони — файли .twig або .php у каталозі views; у контролері відповідь збирається через ViewResponseFactoryInterface::create() (див. Відповіді → HTML).

Низькорівневий рендер — ViewInterface::render() (сервіси, error handler). Шляхи до views налаштовуються в config/view.php і через ComponentInterface::viewPaths() / viewContexts() компонентів.

Простори імен Twig

Шаблон з контролера вказується як @namespace/шлях/без_розширення. Namespace реєструється в конфігурації:

// 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'),
    ];
}

Файл src/Components/AuthAdmin/Views/users/list.twig@auth-admin/users/list.

Каталог resources/views (PathName::VIEWS у paths.php) також додано як fallback без namespace — рідко потрібен, якщо всі шаблони в namespaced paths.

Рендер з контролера

return $this->viewResponse->create('@frontend/index');

return $this->viewResponse->create('@cabinet/index', [
    'user' => $user,
    'latestProject' => $latestProject,
]);

Другий аргумент — змінні лише для цього запиту. Вони зливаються з глобальним контекстом (errors, old, flashes, csrf_token) з ShareViewDataMiddleware.

Layouts і partials

Типова структура:

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' %}

Компонентні сторінки часто наслідують спільний layout іншого namespace (наприклад адмін: @auth-admin/users/list.twig@dashboard/layouts/base.twig).

URL у шаблонах

У skeleton функції uri(), url(), base_url() реєструє 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() — відносний шлях за іменем маршруту (UrlGeneratorInterface::uri())
  • url() — повний URL (UrlGeneratorInterface::url())
  • base_url() — база застосунку (UrlGeneratorInterface::base())

Детальніше про імена маршрутів — маршрутизація.

Глобальні змінні та flash

ЗміннаДжерело
errorsПомилки валідації (flash)
oldПопередній ввід після failed validation
flashesУсі flash-повідомлення (success, error…)
csrf_tokenТокен для форм
route_namespaceКонтекст UI за префіксом URI — детально
<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 повідомлення:

{% 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 %}

Детальніше про валідацію — Запити та валідація.

route_namespace

route_namespaceне те саме, що Twig-namespace шаблону (@cabinet/index). Це логічний контекст інтерфейсу: за поточним URL визначається, в якому «оформленні» працює сторінка (публічний сайт, адмінка, кабінет).

Змінна доступна у всіх шаблонах як глобальна: її додає ContextExtension через ViewContextResolver.

Навіщо

У одному застосунку кілька зон з різними layout і стилями. При 404/500 або в спільних partial потрібно знати, який набір views підключати, без перевірки шляху в кожному Twig-файлі.

route_namespace відповідає: префікс URI → ім’я контексту з конфігурації.

Як визначається

  1. У view.contexts (застосунок + компоненти) задають мапу префікс URI → контекст.
  2. ViewContextResolver бере шлях запиту, перебирає префікси від найдовшого (щоб /admin/users збігся з /admin, а не з /).
  3. ContextExtension::getGlobals() повертає route_namespace у Twig.
// config/view.php
'contexts' => [
    '/' => 'frontend',
],

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

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

Приклад: сторінки помилок

Можна використати один диспетчер у resources/views/errors/ який підключає 404/500 відповідного контексту:

{# resources/views/errors/404.twig #}
{% include '@' ~ (route_namespace|default('frontend')) ~ '/errors/404.twig' %}
  • /unknown-pagefrontend@frontend/errors/404.twig (публічний layout)
  • /admin/missingdashboard@dashboard/errors/404.twig (layout адмінки)

Публічна 404 наслідує @frontend/layouts/base.twig; адмінська — @dashboard/layouts/error.twig. Без route_namespace довелося б дублювати умови «якщо /admin — один шаблон, інакше інший».

Інше використання в 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 %}

Важливий нюанс

Значення в contexts (dashboard, frontend, cabinet) має відповідати Twig-namespace, де реально лежать потрібні шаблони.

У AuthAdmin контекст названо dashboard, а не auth-admin: сторінки помилок і спільний layout адмінки — у @dashboard/, а контент CRUD — у @auth-admin/.

Для /cabinet контекст cabinet, але окремих Views/errors/ у компоненті UserCabinet немає. Якщо потрібна своя 404 у кабінеті — додайте @cabinet/errors/404.twig або змініть диспетчер із fallback на frontend.

Конфігурація config/view.php

return [
    'view' => [
        'extensions' => [
            AppExtension::class,      // has_component()
            ContextExtension::class,  // route_namespace global
            UrlExtension::class,      // uri(), url(), base_url()
        ],
        'paths' => [ /* namespace => шлях від кореня проєкту */ ],
        'contexts' => [
            '/' => 'frontend',       // URI prefix => ім'я контексту для route_namespace
        ],
        'cache_dir' => 'views',      // storage/cache/views у production
    ],
];
  • extensions — класи Twig\Extension\AbstractExtension, резолвяться з DI-контейнера
  • paths — Twig namespaces для застосунку
  • contexts — префікс URI → route_namespace (детально)
  • cache_dir — підкаталог у storage/cache; при app.debug кеш Twig вимкнено

Views у компонентах

Компонент оголошує views через методи viewPaths(), viewContexts() і опційно viewExtensions(). Під час ComponentsServiceProvider::boot() дані додаються до реєстрів і підхоплюються Twig при HTTP-запиті (не в CLI).

// AuthAdminComponent — власне розширення + шаблони
public function viewExtensions(): array
{
    return [TwigExtension::class];
}

public function viewPaths(): array
{
    return [
        'auth-admin' => $this->pathManager->toRelative($this->componentDir . '/Views'),
    ];
}

public function viewContexts(): array
{
    return ['/admin' => 'dashboard'];
}

У Twig: {{ auth().check() }} (якщо підключено розширення компонента), {{ has_component('UserCabinet') }} — з AppExtension.

Власне Twig-розширення

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']),
        ];
    }
}

Зареєструйте клас у view.extensions (застосунок) або в viewExtensions() компонента. У шаблоні: {{ price|format_money }}, {{ format_money(price) }}.

Прямий виклик ViewInterface

/** @var ViewInterface $view */
$view = $container->get(ViewInterface::class);
$html = $view->render('@frontend/errors/404', ['exception' => $e]);

Розширення .twig додається автоматично, якщо не вказано. Телеметрія: TelemetryEvent::TPL_RENDERED у TwigView::render() (Телеметрія). У debug — DebugExtension і Twig Profiler.

Сторінки помилок

Диспетчер resources/views/errors/{код}.twig через route_namespace підключає @frontend/errors/... або @dashboard/errors/.... У production error handler рендерить той самий шлях; якщо файлу немає — fallback на 500.

Додаткові fallback-файли PHP — resources/views/errors/fallback/ (PathName::ERRORS_FALLBACK_VIEWS).

Статика в шаблонах

CSS/JS зазвичай лежать у public/ і підключаються абсолютними шляхами (/assets/css/frontend.css, /vendor/bootstrap/...). Компоненти можуть публікувати assets через assets() (копіювання в public/components/... командою component:publish-assets).

Логіку (запити до БД, права доступу) тримайте в контролерах і сервісах. У Twig — відображення, умови, цикли та допоміжні фільтри.