Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.29% covered (warning)
63.29%
50 / 79
62.50% covered (warning)
62.50%
10 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComponentHelperExtension
63.29% covered (warning)
63.29%
50 / 79
62.50% covered (warning)
62.50%
10 / 16
66.78
0.00% covered (danger)
0.00%
0 / 1
 selectComponent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 deselectComponent
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 apieConstant
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 translate
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
4.59
 safeJsonEncode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFilters
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFunctions
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 renderValidationError
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 getCurrentContext
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getCurrentRenderer
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getCurrentComponent
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 assetContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assetUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 property
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 component
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isPrototyped
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2namespace Apie\TwigTemplateLayoutRenderer\Extension;
3
4use Apie\Core\ApieLib;
5use Apie\Core\Context\ApieContext;
6use Apie\Core\Translator\ApieTranslator;
7use Apie\Core\Translator\ApieTranslatorInterface;
8use Apie\Core\Translator\Lists\TranslationStringSet;
9use Apie\Core\Translator\ValueObjects\AbstractTranslation;
10use Apie\Core\Translator\ValueObjects\TranslationString;
11use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
12use Apie\HtmlBuilders\Interfaces\ComponentInterface;
13use Apie\TwigTemplateLayoutRenderer\TwigRenderer;
14use LogicException;
15use ReflectionClass;
16use Twig\Environment;
17use Twig\Extension\AbstractExtension;
18use Twig\Runtime\EscaperRuntime;
19use Twig\TwigFilter;
20use Twig\TwigFunction;
21
22class ComponentHelperExtension extends AbstractExtension
23{
24    /** @var ComponentInterface[] */
25    private array $componentsHandled = [];
26
27    /** @var TwigRenderer[] */
28    private array $renderers = [];
29
30    /** @var ApieContext[] */
31    private array $contexts = [];
32
33    public function selectComponent(
34        TwigRenderer $renderer,
35        ComponentInterface $component,
36        ApieContext $apieContext
37    ): void {
38        $this->renderers[] = $renderer;
39        $this->componentsHandled[] = $component;
40        $this->contexts[] = $apieContext;
41    }
42
43    public function deselectComponent(ComponentInterface $component): void
44    {
45        if (end($this->componentsHandled) !== $component) {
46            throw new LogicException('Last component is not the one being deselected');
47        }
48        array_pop($this->componentsHandled);
49        array_pop($this->renderers);
50        array_pop($this->contexts);
51    }
52
53    public function apieConstant(string $constantName): mixed
54    {
55        $refl = new ReflectionClass(ApieLib::class);
56        return $refl->getConstant($constantName);
57    }
58
59    public function translate(string|AbstractTranslation|TranslationStringSet $translation): string
60    {
61        $apieContext = $this->getCurrentContext();
62        $translator = $apieContext->getContext(ApieTranslatorInterface::class, false) ?? ApieTranslator::create();
63        try {
64            return $translator->getGeneralTranslation(
65                $apieContext,
66                is_string($translation)
67                    ? new TranslationString($translation)
68                    : $translation
69            ) ?? $translation;
70        } catch (InvalidStringForValueObjectException) {
71            if ($translation instanceof TranslationStringSet) {
72                return $translation->first();
73            }
74            return (string) $translation;
75        }
76    }
77
78    public function safeJsonEncode(mixed $data): string
79    {
80        return json_encode($data, JSON_HEX_QUOT|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS);
81    }
82
83    public function getFilters(): array
84    {
85        return [
86            new TwigFilter('safe_json_encode', [$this, 'safeJsonEncode'], ['is_safe' => ['all']]),
87        ];
88    }
89
90    public function getFunctions(): array
91    {
92        return [
93            new TwigFunction('component', [$this, 'component'], ['is_safe' => ['all']]),
94            new TwigFunction('isPrototyped', [$this, 'isPrototyped']),
95            new TwigFunction('apieConstant', [$this, 'apieConstant']),
96            new TwigFunction('translate', [$this, 'translate'], []),
97            new TwigFunction('property', [$this, 'property'], []),
98            new TwigFunction('assetUrl', [$this, 'assetUrl'], []),
99            new TwigFunction('assetContent', [$this, 'assetContent'], ['is_safe' => ['all']]),
100            new TwigFunction(
101                'renderValidationError',
102                [$this, 'renderValidationError'],
103                ['needs_environment' => true, 'is_safe' => ['all']]
104            ),
105        ];
106    }
107
108    public function renderValidationError(
109        Environment $env,
110        mixed $value,
111        array|string|null $validationError
112    ): string {
113        if ($validationError === null) {
114            return '';
115        }
116        $escaper = $env->getRuntime(EscaperRuntime::class);
117        $valueAttr = '';
118        $valueScript = '';
119        if ($value !== null) {
120            if (is_string($value)) {
121                $valueAttr = ' exact-match="' . $escaper->escape($value, 'html_attr', null, false) . '"';
122            } else {
123                $valueAttr = ' class="unhandled-constraint"';
124                $valueScript = '<script>
125(function (elm) {
126    elm.classList.remove("unhandled-constraint");
127    elm.value = ' . str_replace('<', '&lt;', json_encode($value)) . ';
128}(document.querySelector(".unhandled-constraint"));
129                </script>';
130            }
131        }
132
133        if (is_string($validationError)) {
134            $escapedValidationError = $escaper->escape($validationError, 'html_attr', null, false);
135            return '<apie-constraint-check-definition message="'
136                . $escapedValidationError
137                . '"'
138                . $valueAttr
139                . '></apie-constraint-check-definition>'
140                . $valueScript;
141        }
142        return '';
143    }
144
145    private function getCurrentContext(): ApieContext
146    {
147        if (empty($this->contexts)) {
148            throw new LogicException('No component is selected');
149        }
150        return end($this->contexts);
151    }
152
153    private function getCurrentRenderer(): TwigRenderer
154    {
155        if (empty($this->renderers)) {
156            throw new LogicException('No component is selected');
157        }
158        return end($this->renderers);
159    }
160
161    private function getCurrentComponent(): ComponentInterface
162    {
163        if (empty($this->componentsHandled)) {
164            throw new LogicException('No component is selected');
165        }
166        return end($this->componentsHandled);
167    }
168
169    public function assetContent(string $filename): string
170    {
171        return $this->getCurrentRenderer()->getAssetContents($filename);
172    }
173
174    public function assetUrl(string $filename): string
175    {
176        return $this->getCurrentRenderer()->getAssetUrl($filename);
177    }
178
179    public function property(string $attributeKey): mixed
180    {
181        return $this->getCurrentComponent()->getAttribute($attributeKey);
182    }
183
184    public function component(string $componentName): string
185    {
186        return $this->getCurrentRenderer()->render(
187            $this->getCurrentComponent()->getComponent($componentName),
188            $this->getCurrentContext()
189        );
190    }
191
192    public function isPrototyped(): bool
193    {
194        $attrs = $this->getCurrentComponent()->getAttribute('additionalAttributes');
195        return is_array($attrs) ? ((bool) ($attrs['prototyped'] ?? false)) : false;
196    }
197}