模板高级 #

一、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 .= ' &gt; ';
            }
            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