Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
38 / 40
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
TwigWrapper
95.00% covered (success)
95.00%
38 / 40
90.00% covered (success)
90.00%
9 / 10
19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
6
 setSetup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadTemplate
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 addExtension
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addFunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtension
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTemplateSetName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTemplateSetName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveTemplateSetName
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * This class is just a wrapper for Twig v3
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2023-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2023-05-27
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Twig;
21
22use phpMyFAQ\Core\Exception;
23use phpMyFAQ\Environment as phpMyFAQEnvironment;
24use phpMyFAQ\System;
25use phpMyFAQ\Twig\Extensions\AssetVersionTwigExtension;
26use phpMyFAQ\Twig\Extensions\PluginTwigExtension;
27use phpMyFAQ\Twig\Extensions\TranslateTwigExtension;
28use Twig\Environment;
29use Twig\Error\LoaderError;
30use Twig\Error\RuntimeError;
31use Twig\Error\SyntaxError;
32use Twig\Extension\AttributeExtension;
33use Twig\Extension\DebugExtension;
34use Twig\Extension\ExtensionInterface;
35use Twig\Loader\FilesystemLoader;
36use Twig\TemplateWrapper;
37use Twig\TwigFilter;
38use Twig\TwigFunction;
39
40class TwigWrapper
41{
42    private readonly Environment $twigEnvironment;
43
44    /** @var string Name of an active template set. */
45    private static string $templateSetName = 'default';
46
47    /**
48     * @throws LoaderError
49     */
50    public function __construct(
51        string $templatePath,
52        private bool $isSetup = false,
53        ?string $templateSetName = null,
54    ) {
55        $resolvedTemplateSet = $this->resolveTemplateSetName($templatePath, $templateSetName);
56        self::$templateSetName = $resolvedTemplateSet;
57
58        $filesystemLoader = new FilesystemLoader();
59        $filesystemLoader->addPath($templatePath . '/' . $resolvedTemplateSet);
60        $filesystemLoader->addPath($templatePath . '/admin', namespace: 'admin');
61        $filesystemLoader->addPath($templatePath . '/setup', namespace: 'setup');
62
63        /** @var mixed $configuredCacheDir */
64        $configuredCacheDir = phpMyFAQEnvironment::get('TWIG_CACHE_DIR');
65        $this->twigEnvironment = new Environment($filesystemLoader, [
66            'debug' => System::isDevelopmentVersion(),
67            'cache' => TwigCacheResolver::resolve(
68                debug: System::isDevelopmentVersion() || phpMyFAQEnvironment::isDebugMode(),
69                enabled: phpMyFAQEnvironment::get('TWIG_CACHE_ENABLED'),
70                configuredDir: is_string($configuredCacheDir) ? $configuredCacheDir : null,
71                defaultDir: (string) PMF_ROOT_DIR . '/cache/twig',
72            ),
73            // Recompile when a template file changes, so self-hosted template
74            // edits are picked up without a manual cache clear.
75            'auto_reload' => true,
76        ]);
77
78        // Always add the translation and asset-versioning extensions
79        $this->twigEnvironment->addExtension(new AttributeExtension(TranslateTwigExtension::class));
80        $this->twigEnvironment->addExtension(new AttributeExtension(AssetVersionTwigExtension::class));
81
82        // Add the plugin extension if it's not in the setup phase
83        if (!$this->isSetup) {
84            $this->twigEnvironment->addExtension(new AttributeExtension(PluginTwigExtension::class));
85        }
86
87        // If we're on a development version or debug is enabled, add the debug extension
88        if (System::isDevelopmentVersion() || phpMyFAQEnvironment::isDebugMode()) {
89            $this->twigEnvironment->addExtension(new DebugExtension());
90        }
91    }
92
93    public function setSetup(bool $isSetup): void
94    {
95        $this->isSetup = $isSetup;
96    }
97
98    /**
99     * @throws Exception
100     */
101    public function loadTemplate(string $templateFile): TemplateWrapper
102    {
103        try {
104            return $this->twigEnvironment->load($templateFile);
105        } catch (LoaderError|RuntimeError|SyntaxError $exception) {
106            throw new Exception($exception->getMessage());
107        }
108    }
109
110    public function addExtension(ExtensionInterface $extension): void
111    {
112        $this->twigEnvironment->addExtension($extension);
113    }
114
115    public function addFunction(TwigFunction $twigFunction): void
116    {
117        $this->twigEnvironment->addFunction($twigFunction);
118    }
119
120    /**
121     * @param class-string<ExtensionInterface> $class
122     */
123    public function getExtension(string $class): ExtensionInterface
124    {
125        return $this->twigEnvironment->getExtension($class);
126    }
127
128    public function addFilter(TwigFilter $twigFilter): void
129    {
130        $this->twigEnvironment->addFilter($twigFilter);
131    }
132
133    /**
134     * Returns the name of the actual template set.
135     */
136    public static function getTemplateSetName(): string
137    {
138        return self::$templateSetName;
139    }
140
141    /**
142     * Set the template set name to use.
143     */
144    public static function setTemplateSetName(string $tplSetName = 'default'): void
145    {
146        self::$templateSetName = $tplSetName;
147    }
148
149    private function resolveTemplateSetName(string $templatePath, ?string $templateSetName): string
150    {
151        $requestedTemplateSet = trim($templateSetName ?? self::$templateSetName);
152        if ($requestedTemplateSet === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $requestedTemplateSet)) {
153            $requestedTemplateSet = 'default';
154        }
155
156        $requestedPath = $templatePath . '/' . $requestedTemplateSet;
157        if (is_dir($requestedPath)) {
158            return $requestedTemplateSet;
159        }
160
161        return 'default';
162    }
163}