模板高级 #
一、Twig扩展 #
1.1 创建Twig扩展 #
php
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class AppExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('price', [$this, 'formatPrice']),
new TwigFilter('truncate', [$this, 'truncate'], ['is_safe' => ['html']]),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('asset_version', [$this, 'getAssetVersion']),
new TwigFunction('is_active', [$this, 'isActive']),
];
}
public function formatPrice(float $price, string $currency = 'CNY'): string
{
$symbols = [
'CNY' => '¥',
'USD' => '$',
'EUR' => '€',
];
$symbol = $symbols[$currency] ?? $currency;
return $symbol . number_format($price, 2);
}
public function truncate(string $text, int $length = 100, string $suffix = '...'): string
{
if (strlen($text) <= $length) {
return $text;
}
return substr($text, 0, $length) . $suffix;
}
public function getAssetVersion(string $path): string
{
return $path . '?v=' . filemtime($this->projectDir . '/public/' . $path);
}
public function isActive(string $routeName): bool
{
return $this->requestStack->getCurrentRequest()->attributes->get('_route') === $routeName;
}
}
1.2 注册扩展 #
yaml
# config/services.yaml
services:
App\Twig\AppExtension:
tags: ['twig.extension']
1.3 使用扩展 #
twig
{# 使用自定义过滤器 #}
{{ product.price|price }}
{{ product.price|price('USD') }}
{{ article.content|truncate(200) }}
{# 使用自定义函数 #}
<link rel="stylesheet" href="{{ asset_version('css/style.css') }}">
<li class="{{ is_active('app_home') ? 'active' : '' }}">
<a href="{{ path('app_home') }}">首页</a>
</li>
二、自定义过滤器 #
2.1 基本过滤器 #
php
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class FilterExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('md5', [$this, 'md5Filter']),
new TwigFilter('slug', [$this, 'slugFilter']),
new TwigFilter('highlight', [$this, 'highlightFilter'], ['is_safe' => ['html']]),
new TwigFilter('time_ago', [$this, 'timeAgoFilter']),
];
}
public function md5Filter(string $string): string
{
return md5($string);
}
public function slugFilter(string $string): string
{
return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $string), '-'));
}
public function highlightFilter(string $text, string $keyword): string
{
return preg_replace(
'/(' . preg_quote($keyword, '/') . ')/i',
'<mark>$1</mark>',
$text
);
}
public function timeAgoFilter(\DateTimeInterface $date): string
{
$now = new \DateTime();
$diff = $now->diff($date);
if ($diff->y > 0) {
return $diff->y . '年前';
}
if ($diff->m > 0) {
return $diff->m . '个月前';
}
if ($diff->d > 0) {
return $diff->d . '天前';
}
if ($diff->h > 0) {
return $diff->h . '小时前';
}
if ($diff->i > 0) {
return $diff->i . '分钟前';
}
return '刚刚';
}
}
2.2 使用过滤器 #
twig
{# MD5加密 #}
{{ user.email|md5 }}
{# URL友好化 #}
{{ article.title|slug }}
{# 高亮关键词 #}
{{ article.content|highlight(keyword) }}
{# 相对时间 #}
{{ comment.createdAt|time_ago }}
2.3 过滤器选项 #
php
<?php
public function getFilters(): array
{
return [
new TwigFilter('safe_html', [$this, 'safeHtml'], [
'is_safe' => ['html'],
]),
new TwigFilter('safe_all', [$this, 'safeAll'], [
'is_safe' => ['html', 'js', 'css', 'url'],
]),
new TwigFilter('pre_escape', [$this, 'preEscape'], [
'pre_escape' => 'html',
'is_safe' => ['html'],
]),
];
}
三、自定义函数 #
3.1 基本函数 #
php
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class FunctionExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('config', [$this, 'getConfig']),
new TwigFunction('setting', [$this, 'getSetting']),
new TwigFunction('menu', [$this, 'renderMenu'], ['is_safe' => ['html']]),
new TwigFunction('breadcrumb', [$this, 'renderBreadcrumb'], ['is_safe' => ['html']]),
];
}
public function getConfig(string $key, mixed $default = null): mixed
{
return $this->configService->get($key, $default);
}
public function getSetting(string $key, mixed $default = null): mixed
{
return $this->settingService->get($key, $default);
}
public function renderMenu(string $name): string
{
$menu = $this->menuService->getMenu($name);
$html = '<ul class="menu">';
foreach ($menu as $item) {
$html .= sprintf(
'<li><a href="%s">%s</a></li>',
$item['url'],
$item['label']
);
}
$html .= '</ul>';
return $html;
}
public function renderBreadcrumb(): string
{
$items = $this->breadcrumbService->getItems();
$html = '<nav class="breadcrumb">';
foreach ($items as $i => $item) {
if ($i > 0) {
$html .= ' > ';
}
if ($item['url']) {
$html .= sprintf('<a href="%s">%s</a>', $item['url'], $item['label']);
} else {
$html .= $item['label'];
}
}
$html .= '</nav>';
return $html;
}
}
3.2 使用函数 #
twig
{# 获取配置 #}
<title>{{ config('site.name', 'My Site') }}</title>
{# 获取设置 #}
<p>联系电话: {{ setting('contact.phone') }}</p>
{# 渲染菜单 #}
{{ menu('main') }}
{# 渲染面包屑 #}
{{ breadcrumb() }}
3.3 函数选项 #
php
<?php
public function getFunctions(): array
{
return [
new TwigFunction('needs_context', [$this, 'needsContext'], [
'needs_context' => true,
]),
new TwigFunction('needs_environment', [$this, 'needsEnvironment'], [
'needs_environment' => true,
]),
new TwigFunction('is_safe_html', [$this, 'isSafeHtml'], [
'is_safe' => ['html'],
]),
];
}
public function needsContext(array $context): string
{
return $context['app']->getRequest()->getPathInfo();
}
public function needsEnvironment(\Twig\Environment $env, string $template): string
{
return $env->render($template);
}
四、全局变量 #
4.1 定义全局变量 #
yaml
# config/packages/twig.yaml
twig:
globals:
site_name: 'My Symfony App'
site_version: '1.0.0'
ga_tracking_id: 'UA-XXXXX-Y'
4.2 服务作为全局变量 #
yaml
# config/packages/twig.yaml
twig:
globals:
app_settings: '@App\Service\SettingsService'
app_menu: '@App\Service\MenuService'
4.3 使用全局变量 #
twig
{# 使用全局变量 #}
<title>{{ site_name }}</title>
<meta name="version" content="{{ site_version }}">
{# 使用服务 #}
<p>{{ app_settings.get('contact.email') }}</p>
<nav>{{ app_menu.render('main') }}</nav>
{# 条件使用 #}
{% if ga_tracking_id %}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ ga_tracking_id }}"></script>
{% endif %}
五、Twig运行时 #
5.1 运行时加载器 #
php
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class RuntimeExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('user_avatar', [UserRuntime::class, 'avatar']),
new TwigFunction('user_link', [UserRuntime::class, 'link'], ['is_safe' => ['html']]),
];
}
}
php
<?php
namespace App\Twig;
use App\Service\UserService;
use Twig\Extension\RuntimeExtensionInterface;
class UserRuntime implements RuntimeExtensionInterface
{
public function __construct(
private UserService $userService
) {}
public function avatar(int $userId, int $size = 40): string
{
$user = $this->userService->find($userId);
return $user?->getAvatar() ?? '/images/default-avatar.png';
}
public function link(int $userId): string
{
$user = $this->userService->find($userId);
if (!$user) {
return '<span>未知用户</span>';
}
return sprintf(
'<a href="/user/%d">%s</a>',
$user->getId(),
htmlspecialchars($user->getName())
);
}
}
5.2 注册运行时 #
yaml
# config/services.yaml
services:
App\Twig\UserRuntime:
tags:
- { name: twig.runtime }
六、模板事件 #
6.1 模板渲染事件 #
php
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Twig\Environment;
class TemplateSubscriber implements EventSubscriberInterface
{
public function __construct(
private Environment $twig
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
];
}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$this->twig->addGlobal('current_path', $request->getPathInfo());
$this->twig->addGlobal('current_route', $request->attributes->get('_route'));
}
}
七、模板测试 #
7.1 自定义测试 #
php
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigTest;
class TestExtension extends AbstractExtension
{
public function getTests(): array
{
return [
new TwigTest('online', [$this, 'isOnline']),
new TwigTest('admin', [$this, 'isAdmin']),
new TwigTest('numeric', [$this, 'isNumeric']),
];
}
public function isOnline($user): bool
{
if (!is_object($user)) {
return false;
}
return method_exists($user, 'isOnline') && $user->isOnline();
}
public function isAdmin($user): bool
{
if (!is_object($user)) {
return false;
}
return method_exists($user, 'hasRole') && $user->hasRole('ROLE_ADMIN');
}
public function isNumeric($value): bool
{
return is_numeric($value);
}
}
7.2 使用测试 #
twig
{# 使用自定义测试 #}
{% if user is online %}
<span class="online">在线</span>
{% endif %}
{% if user is admin %}
<a href="{{ path('admin_dashboard') }}">管理后台</a>
{% endif %}
{% if value is numeric %}
<p>数值: {{ value }}</p>
{% endif %}
八、模板标签 #
8.1 自定义标签 #
php
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TokenParser\TokenParserInterface;
class TagExtension extends AbstractExtension
{
public function getTokenParsers(): array
{
return [
new CacheTokenParser(),
];
}
}
class CacheTokenParser implements TokenParserInterface
{
public function parse(\Twig\Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$key = $this->parser->getExpressionParser()->parseExpression();
$ttl = null;
if ($stream->nextIf(\Twig\Token::NAME_TYPE, 'ttl')) {
$ttl = $this->parser->getExpressionParser()->parseExpression();
}
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideCacheEnd'], true);
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
return new CacheNode($key, $ttl, $body, $lineno, $this->getTag());
}
public function getTag(): string
{
return 'cache';
}
public function decideCacheEnd(\Twig\Token $token): bool
{
return $token->test('endcache');
}
}
8.2 使用自定义标签 #
twig
{# 缓存区块 #}
{% cache 'user_list' ttl(3600) %}
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
{% endcache %}
九、性能优化 #
9.1 模板缓存 #
yaml
# config/packages/twig.yaml
twig:
cache: '%kernel.cache_dir%/twig'
auto_reload: '%kernel.debug%'
debug: '%kernel.debug%'
9.2 模板优化建议 #
text
模板优化建议:
├── 避免在模板中进行复杂计算
├── 使用缓存减少渲染开销
├── 合理使用include和embed
├── 避免过多全局变量
├── 使用with_context=false减少变量传递
└── 启用模板编译缓存
十、总结 #
本章学习了:
- 创建Twig扩展
- 自定义过滤器
- 自定义函数
- 全局变量定义
- Twig运行时加载
- 模板事件监听
- 自定义测试
- 自定义标签
- 性能优化
下一章将学习 Doctrine基础。
最后更新:2026-03-28