Issues (16)

Branch: master

src/Renderer/Twig.php (2 issues)

1
<?php
2
3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cecil\Renderer;
15
16
use Cecil\Builder;
17
use Cecil\Exception\RuntimeException;
18
use Cecil\Renderer\Extension\Core as CoreExtension;
19
use Cecil\Util;
20
use Performing\TwigComponents\Configuration;
21
use Symfony\Bridge\Twig\Extension\TranslationExtension;
22
use Symfony\Component\Translation\Formatter\MessageFormatter;
23
use Symfony\Component\Translation\IdentityTranslator;
24
use Symfony\Component\Translation\Translator;
25
use Twig\Extra\Intl\IntlExtension;
26
use Twig\Extra\Cache\CacheExtension;
27
28
/**
29
 * Twig renderer.
30
 *
31
 * This class is responsible for rendering templates using the Twig templating engine.
32
 * It initializes Twig with the necessary configurations, loads extensions, and provides methods
33
 * to render templates, add global variables, and manage translations.
34
 * It also supports debugging and profiling when in debug mode.
35
 */
36
class Twig implements RendererInterface
37
{
38
    /**
39
     * Builder object.
40
     * @var Builder
41
     */
42
    protected $builder;
43
    /**
44
     * Twig environment instance.
45
     * @var \Twig\Environment
46
     */
47
    private $twig;
48
    /**
49
     * Translator instance for translations.
50
     * @var Translator
51
     */
52
    private $translator = null;
53
    /**
54
     * Profile for debugging and profiling.
55
     * @var \Twig\Profiler\Profile
56
     */
57
    private $profile = null;
58
59
    /**
60
     * {@inheritdoc}
61
     */
62 1
    public function __construct(Builder $builder, $templatesPath)
63
    {
64 1
        $this->builder = $builder;
65
        // load layouts
66 1
        $loader = new \Twig\Loader\FilesystemLoader($templatesPath);
67
        // default options
68 1
        $loaderOptions = [
69 1
            'debug'            => $this->builder->isDebug(),
70 1
            'strict_variables' => true,
71 1
            'autoescape'       => false,
72 1
            'auto_reload'      => true,
73 1
            'cache'            => false,
74 1
        ];
75
        // use Twig cache?
76 1
        if ($this->builder->getConfig()->isEnabled('cache.templates')) {
77 1
            $loaderOptions = array_replace($loaderOptions, ['cache' => $this->builder->getConfig()->getCacheTemplatesPath()]);
78
        }
79
        // create the Twig instance
80 1
        $this->twig = new \Twig\Environment($loader, $loaderOptions);
81
        // set date format
82 1
        $this->twig->getExtension(\Twig\Extension\CoreExtension::class)
83 1
            ->setDateFormat((string) $this->builder->getConfig()->get('date.format'));
84
        // set timezone
85 1
        if ($this->builder->getConfig()->has('date.timezone')) {
86
            $this->twig->getExtension(\Twig\Extension\CoreExtension::class)
87
                ->setTimezone($this->builder->getConfig()->get('date.timezone') ?? date_default_timezone_get());
88
        }
89
        /*
90
         * adds extensions
91
         */
92
        // Cecil core extension
93 1
        $this->twig->addExtension(new CoreExtension($this->builder));
94
        // required by `template_from_string()`
95 1
        $this->twig->addExtension(new \Twig\Extension\StringLoaderExtension());
96
        // l10n
97 1
        $this->translator = new Translator(
98 1
            $this->builder->getConfig()->getLanguageProperty('locale'),
99 1
            new MessageFormatter(new IdentityTranslator()),
100 1
            $this->builder->getConfig()->isEnabled('cache.translations') ? $this->builder->getConfig()->getCacheTranslationsPath() : null,
101 1
            $this->builder->isDebug()
102 1
        );
103 1
        if (\count($this->builder->getConfig()->getLanguages()) > 0) {
104 1
            foreach ((array) $this->builder->getConfig()->get('layouts.translations.formats') as $format) {
105 1
                $loader = \sprintf('Symfony\Component\Translation\Loader\%sFileLoader', ucfirst($format));
106 1
                if (class_exists($loader)) {
107 1
                    $this->translator->addLoader($format, new $loader());
108 1
                    $this->builder->getLogger()->debug(\sprintf('Translation loader for format "%s" found', $format));
109
                }
110
            }
111 1
            foreach ($this->builder->getConfig()->getLanguages() as $lang) {
112
                // internal
113 1
                $this->addTransResource($this->builder->getConfig()->getTranslationsInternalPath(), $lang['locale']);
114
                // themes
115 1
                if ($themes = $this->builder->getConfig()->getTheme()) {
116 1
                    foreach ($themes as $theme) {
117 1
                        $this->addTransResource($this->builder->getConfig()->getThemeDirPath($theme, 'translations'), $lang['locale']);
118
                    }
119
                }
120
                // site
121 1
                $this->addTransResource($this->builder->getConfig()->getTranslationsPath(), $lang['locale']);
122
            }
123
        }
124 1
        $this->twig->addExtension(new TranslationExtension($this->translator));
125
        // intl
126 1
        $this->twig->addExtension(new IntlExtension());
127 1
        if (\extension_loaded('intl')) {
128 1
            $this->builder->getLogger()->debug('PHP Intl extension is loaded');
129
        }
130
        // filters fallback
131 1
        $this->twig->registerUndefinedFilterCallback(function ($name) {
132
            switch ($name) {
133 1
                case 'localizeddate':
134 1
                    return new \Twig\TwigFilter($name, function (?\DateTime $value = null) {
135 1
                        return date($this->builder->getConfig()->get('date.format'), $value->getTimestamp());
0 ignored issues
show
The method getTimestamp() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

135
                        return date($this->builder->getConfig()->get('date.format'), $value->/** @scrutinizer ignore-call */ getTimestamp());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
It seems like $this->builder->getConfig()->get('date.format') can also be of type null; however, parameter $format of date() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

135
                        return date(/** @scrutinizer ignore-type */ $this->builder->getConfig()->get('date.format'), $value->getTimestamp());
Loading history...
136 1
                    });
137
            }
138
139
            return false;
140 1
        });
141
        // components
142 1
        Configuration::make($this->twig)
143 1
            ->setTemplatesPath($this->builder->getConfig()->get('layouts.components.dir') ?? 'components')
144 1
            ->setTemplatesExtension($this->builder->getConfig()->get('layouts.components.ext') ?? 'twig')
145 1
            ->useCustomTags()
146 1
            ->setup();
147
        // cache
148 1
        $this->twig->addExtension(new CacheExtension());
149 1
        $this->twig->addRuntimeLoader(new TwigCacheRuntimeLoader($this->builder->getConfig()->getCacheTemplatesPath()));
150
        // debug
151 1
        if ($this->builder->isDebug()) {
152
            // dump()
153 1
            $this->twig->addExtension(new \Twig\Extension\DebugExtension());
154
            // profiler
155 1
            $this->profile = new \Twig\Profiler\Profile();
156 1
            $this->twig->addExtension(new \Twig\Extension\ProfilerExtension($this->profile));
157
        }
158
        // loads custom extensions
159 1
        if ($this->builder->getConfig()->has('layouts.extensions')) {
160 1
            foreach ((array) $this->builder->getConfig()->get('layouts.extensions') as $name => $class) {
161
                try {
162 1
                    $this->twig->addExtension(new $class($this->builder));
163 1
                    $this->builder->getLogger()->debug(\sprintf('Twig extension "%s" added', $name));
164 1
                } catch (RuntimeException | \Error $e) {
165 1
                    $this->builder->getLogger()->error(\sprintf('Unable to add Twig extension "%s": %s', $name, $e->getMessage()));
166
                }
167
            }
168
        }
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174 1
    public function addGlobal(string $name, $value): void
175
    {
176 1
        $this->twig->addGlobal($name, $value);
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 1
    public function render(string $template, array $variables): string
183
    {
184 1
        return $this->twig->render($template, $variables);
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190 1
    public function setLocale(string $locale): void
191
    {
192 1
        if (\extension_loaded('intl')) {
193 1
            \Locale::setDefault($locale);
194
        }
195 1
        $this->translator === null ?: $this->translator->setLocale($locale);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 1
    public function addTransResource(string $translationsDir, string $locale): void
202
    {
203 1
        $locales = [$locale];
204
        // if locale is 'fr_FR', trying to load ['fr', 'fr_FR']
205 1
        if (\strlen($locale) > 2) {
206 1
            array_unshift($locales, substr($locale, 0, 2));
207
        }
208 1
        foreach ($locales as $locale) {
209 1
            foreach ((array) $this->builder->getConfig()->get('layouts.translations.formats') as $format) {
210 1
                $translationFile = Util::joinPath($translationsDir, \sprintf('messages.%s.%s', $locale, $format));
211 1
                if (Util\File::getFS()->exists($translationFile)) {
212 1
                    $this->translator->addResource($format, $translationFile, $locale);
213 1
                    $this->builder->getLogger()->debug(\sprintf('Translation file "%s" added', $translationFile));
214
                }
215
            }
216
        }
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 1
    public function getDebugProfile(): ?\Twig\Profiler\Profile
223
    {
224 1
        return $this->profile;
225
    }
226
227
    /**
228
     * Returns the Twig instance.
229
     */
230
    public function getTwig(): \Twig\Environment
231
    {
232
        return $this->twig;
233
    }
234
}
235