Отладка Twig и управление ассетами
Отладка шаблонов: dump()
Функция dump() -- основной инструмент отладки в Twig. Работает только в dev и test окружениях.
{# Dump all variables available in template #}
{{ dump() }}
{# Dump specific variable #}
{{ dump(user) }}
{# Dump multiple variables #}
{{ dump(user, order, cart) }}
{# Dump nested property #}
{{ dump(order.items) }}
{# Dump inside a loop #}
{% for item in order.items %}
{{ dump(item) }}
{% endfor %}
dump() vs dd() vs dump() в PHP
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
final class DebugController extends AbstractController
{
public function show(): Response
{
$data = ['key' => 'value'];
// dump() -- outputs and continues execution
dump($data);
// dd() -- "dump and die", stops execution
dd($data);
// In Twig: {{ dump(variable) }} -- rendered as HTML in template
return $this->render('debug/show.html.twig', [
'data' => $data,
]);
}
}
Подвох экзамена:
dump()в Twig доступна ТОЛЬКО когдаdebug: trueв конфигурации (обычно dev/test). В productiondump()вызовет ошибку. Это обеспечивает безопасность -- чувствительные данные не утекут в production.
Тег dump (без вывода в шаблон)
{# dump as tag -- output goes to Web Debug Toolbar, not to page #}
{% dump user %}
{% dump order.items %}
{# This is cleaner -- doesn't pollute the rendered page #}
{# Data appears in Profiler's "Dump" panel #}
Web Debug Toolbar
Web Debug Toolbar (WDT) -- панель внизу страницы в dev-окружении. Показывает информацию о текущем запросе.
Панели WDT
| Панель | Информация |
|---|---|
| Performance | Время выполнения, потребление памяти |
| Request | Контроллер, маршрут, параметры, формат |
| Twig | Количество шаблонов, время рендеринга, вызовы блоков |
| Doctrine | SQL-запросы, количество, время |
| Security | Текущий пользователь, роли, firewall |
| Forms | Структура форм, данные, ошибки валидации |
| Translation | Использованные переводы, пропущенные ключи |
| Cache | Hits/misses, operations |
| Logger | Логи текущего запроса |
| Dump | Переменные из dump() и {% dump %} |
Включение/отключение WDT
# config/packages/web_profiler.yaml
when@dev:
web_profiler:
toolbar: true # Show toolbar
intercept_redirects: false # Don't intercept redirects
when@test:
web_profiler:
toolbar: false # Disable in tests
collect: false
Profiler
<?php
declare(strict_types=1);
// Profiler stores detailed data for each request
// Access at: /_profiler/ (in dev environment)
// You can link to the profiler in Twig:
// {{ render(controller('web_profiler.controller.profiler::toolbarAction')) }}
{# Profiler URL for current request #}
<a href="{{ path('_profiler', {token: app.request.attributes.get('_stopwatch_token')}) }}">
Open Profiler
</a>
{# In Twig panel of Profiler, you can see: #}
{# - All rendered templates #}
{# - Template hierarchy (extends, includes) #}
{# - Block rendering times #}
{# - Template variables #}
Twig Profiler Panel
Profiler Twig panel показывает:
- Все отрендеренные шаблоны (включая вложенные)
- Иерархию наследования шаблонов
- Время рендеринга каждого шаблона
- Количество вызовов блоков
- Использование
include,embed,render
{# Stopwatch -- measure specific template sections #}
{% stopwatch 'heavy_section' %}
{# This section will appear in Profiler Performance timeline #}
{% for product in products %}
{% include 'product/_card.html.twig' %}
{% endfor %}
{% endstopwatch %}
Подвох экзамена: Тег
{% stopwatch %}работает только при наличии компонентаStopwatch. Он добавляет секции в timeline Profiler. В production этот тег безопасно игнорируется.
Управление ассетами: AssetMapper
AssetMapper -- современный подход к управлению ассетами в Symfony (начиная с 6.3). Не требует Node.js, npm или сборщиков.
Установка и конфигурация
composer require symfony/asset-mapper
# config/packages/asset_mapper.yaml
framework:
asset_mapper:
paths:
- assets/
missing_import_mode: strict # Error on missing imports
Структура ассетов
assets/
styles/
app.css
_variables.css
controllers/
hello_controller.js
app.js
bootstrap.js
Использование в Twig
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
{# AssetMapper generates importmap and preloads #}
{% block importmap %}{{ importmap('app') }}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
// assets/app.js
import './styles/app.css';
// Import from vendor (installed via importmap:require)
import { Application } from '@hotwired/stimulus';
import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers';
const app = Application.start();
Команды AssetMapper
# Install a JavaScript package
php bin/console importmap:require bootstrap
php bin/console importmap:require lodash
# List mapped assets
php bin/console debug:asset-map
# Compile assets for production (versioned, fingerprinted)
php bin/console asset-map:compile
Функция asset() в Twig
{# Reference static files #}
<img src="{{ asset('images/logo.png') }}" alt="Logo">
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{# With versioned assets (cache busting) #}
{# Generates: /assets/images/logo-a1b2c3d4.png #}
<img src="{{ asset('images/logo.png') }}">
# config/packages/framework.yaml
framework:
assets:
version: '1.0.0' # Manual versioning
# OR
version_strategy: json_manifest # Manifest-based versioning
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
Подвох экзамена:
asset()в Twig генерирует URL к статическим файлам. Сversion_strategy: json_manifestчитает маппинг из manifest.json. AssetMapper автоматически добавляет content hash к именам файлов для cache busting. Без AssetMapper или Encore --asset()просто возвращает путь как есть.
Webpack Encore (альтернатива AssetMapper)
Webpack Encore -- обёртка над Webpack для Symfony. Требует Node.js.
composer require symfony/webpack-encore-bundle
npm install
// webpack.config.js
const Encore = require('@symfony/webpack-encore');
Encore
.setOutputPath('public/build/')
.setPublicPath('/build')
.addEntry('app', './assets/app.js')
.addStyleEntry('app_styles', './assets/styles/app.css')
.enableStimulusBridge('./assets/controllers.json')
.splitEntryChunks()
.enableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
.enableSassLoader() // Optional: SASS support
.enableTypeScriptLoader() // Optional: TypeScript support
;
module.exports = Encore.getWebpackConfig();
Encore в Twig
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>
AssetMapper vs Webpack Encore
| Аспект | AssetMapper | Webpack Encore |
|---|---|---|
| Node.js | Не нужен | Нужен |
| Сборка | Нет (native ESM) | Webpack |
| SASS/TypeScript | Нет (используйте нативные CSS/JS) | Да |
| Tree shaking | Браузер (HTTP/2) | Webpack |
| Hot reload | Нет | Да |
| Production | asset-map:compile |
npm run build |
| Рекомендация | Новые проекты Symfony 7+ | Legacy или сложные сборки |
Создание Twig Extension
Кастомный фильтр
<?php
declare(strict_types=1);
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
final class AppExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
// Simple filter: value is first argument
new TwigFilter('price', $this->formatPrice(...)),
// Filter with environment access
new TwigFilter('highlight', $this->highlight(...), [
'is_safe' => ['html'], // Output is not auto-escaped
]),
// Filter with pre_escape (escape input BEFORE filter)
new TwigFilter('nl2br_safe', $this->nl2br(...), [
'pre_escape' => 'html',
'is_safe' => ['html'],
]),
];
}
public function getFunctions(): array
{
return [
// Function with HTML-safe output
new TwigFunction('svg_icon', $this->renderSvgIcon(...), [
'is_safe' => ['html'],
]),
// Function that needs Twig Environment
new TwigFunction('render_widget', $this->renderWidget(...), [
'needs_environment' => true,
]),
// Function that needs template context
new TwigFunction('debug_context', $this->debugContext(...), [
'needs_context' => true,
]),
];
}
public function formatPrice(float $amount, string $currency = 'RUB'): string
{
return number_format($amount, 2, ',', ' ') . ' ' . $currency;
}
public function highlight(string $text, string $query): string
{
// Safely highlight search term in text
$escaped = htmlspecialchars($query, ENT_QUOTES, 'UTF-8');
return str_ireplace(
$escaped,
'<mark>' . $escaped . '</mark>',
htmlspecialchars($text, ENT_QUOTES, 'UTF-8'),
);
}
public function nl2br(string $text): string
{
// Input is already escaped (pre_escape), just convert newlines
return nl2br($text);
}
public function renderSvgIcon(string $name, int $size = 24): string
{
return sprintf(
'<svg width="%d" height="%d"><use href="/icons.svg#%s"/></svg>',
$size,
$size,
htmlspecialchars($name, ENT_QUOTES),
);
}
public function renderWidget(
\Twig\Environment $env,
string $widget,
): string {
// Access Twig environment for advanced operations
return $env->render('widgets/' . $widget . '.html.twig');
}
public function debugContext(array $context): string
{
// Access all template variables
return implode(', ', array_keys($context));
}
}
{# Using custom filters and functions #}
{{ product.price|price }} {# "1 234,56 RUB" #}
{{ product.price|price('USD') }} {# "1 234,56 USD" #}
{{ article.body|highlight(search_query) }}
{{ "Line 1\nLine 2"|nl2br_safe }}
{{ svg_icon('search', 20) }}
{{ render_widget('recent_posts') }}
{{ debug_context() }}
Lazy Runtime Extension
<?php
declare(strict_types=1);
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
// Extension -- lightweight, no dependencies
final class SlugExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('slugify', [SlugRuntime::class, 'slugify']),
];
}
}
<?php
declare(strict_types=1);
namespace App\Twig;
use Symfony\Component\String\Slugger\SluggerInterface;
use Twig\Extension\RuntimeExtensionInterface;
// Runtime -- heavy dependencies, loaded only when filter is used
final class SlugRuntime implements RuntimeExtensionInterface
{
public function __construct(
private readonly SluggerInterface $slugger,
) {}
public function slugify(string $text): string
{
return $this->slugger->slug($text)->lower()->toString();
}
}
Подвох экзамена: Extension регистрируется при каждом рендере шаблона. Runtime загружается ТОЛЬКО при вызове фильтра/функции. Для Extension с зависимостями (сервисы, репозитории) ВСЕГДА используйте Runtime паттерн для оптимальной производительности.
Кастомный тест (Twig Test)
<?php
declare(strict_types=1);
namespace App\Twig;
use App\Entity\User;
use Twig\Extension\AbstractExtension;
use Twig\TwigTest;
final class UserTestExtension extends AbstractExtension
{
public function getTests(): array
{
return [
new TwigTest('admin', $this->isAdmin(...)),
new TwigTest('active', $this->isActive(...)),
];
}
private function isAdmin(mixed $user): bool
{
return $user instanceof User && in_array('ROLE_ADMIN', $user->getRoles(), true);
}
private function isActive(mixed $user): bool
{
return $user instanceof User && $user->isActive();
}
}
{# Using custom tests #}
{% if user is admin %}
<a href="{{ path('admin_dashboard') }}">Admin Panel</a>
{% endif %}
{% if user is not active %}
<span class="badge-inactive">Account disabled</span>
{% endif %}
Отладка через консольные команды
# List all Twig filters, functions, tests, globals
php bin/console debug:twig
# Show details about specific filter/function
php bin/console debug:twig --filter=price
php bin/console debug:twig --function=asset
# List all registered Twig paths (namespaces)
php bin/console debug:twig --paths
# Show template for form theme
php bin/console debug:twig --theme
Lint шаблонов
# Check all templates for syntax errors
php bin/console lint:twig templates/
# Check specific file
php bin/console lint:twig templates/base.html.twig
# Check from stdin
echo '{{ foo }' | php bin/console lint:twig -
Именованные пространства шаблонов
# config/packages/twig.yaml
twig:
paths:
'%kernel.project_dir%/templates': '' # Default namespace
'%kernel.project_dir%/templates/emails': email # @email namespace
'%kernel.project_dir%/src/Admin/templates': admin # @admin namespace
{# Using namespaced templates #}
{% extends '@admin/layout.html.twig' %}
{% include '@email/order_confirmation.html.twig' %}
{# Bundle templates use bundle name as namespace #}
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
Итоги
dump()-- основной инструмент отладки; работает только приdebug: true{% dump var %}-- вывод в Profiler, не в страницу{% stopwatch %}-- измерение производительности секций шаблона- Web Debug Toolbar: request, Twig, Doctrine, forms, translations, security
- AssetMapper: без Node.js, native ESM,
{{ importmap('app') }} - Webpack Encore: с Node.js, SASS/TS,
{{ encore_entry_link_tags('app') }} asset(): путь к статическому файлу с поддержкой версионирования- Extensions:
getFilters(),getFunctions(),getTests() - Runtime Extension: lazy-загрузка зависимостей через
RuntimeExtensionInterface needs_environment,needs_context,is_safe,pre_escape-- опции фильтров/функций