Lines 95.00% 38 / 40
Methods 90.00% 9 / 10
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 23 / 23 100.00% 1 / 1 6
 setSetup 100.00% 1 / 1 100.00% 1 / 1 1
 loadTemplate 33.33% 1 / 3 0.00% 0 / 1 3.19
 addExtension 100.00% 1 / 1 100.00% 1 / 1 1
 addFunction 100.00% 1 / 1 100.00% 1 / 1 1
 getExtension 100.00% 1 / 1 100.00% 1 / 1 1
 addFilter 100.00% 1 / 1 100.00% 1 / 1 1
 getTemplateSetName 100.00% 1 / 1 100.00% 1 / 1 1
 setTemplateSetName 100.00% 1 / 1 100.00% 1 / 1 1
 resolveTemplateSetName 100.00% 7 / 7 100.00% 1 / 1 4
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}