• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tempestphp / tempest-framework / 14024978163

23 Mar 2025 05:55PM UTC coverage: 79.391% (-0.05%) from 79.441%
14024978163

push

github

web-flow
feat(view): cache Blade and Twig templates in internal storage (#1061)

2 of 2 new or added lines in 2 files covered. (100.0%)

912 existing lines in 110 files now uncovered.

10478 of 13198 relevant lines covered (79.39%)

91.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.73
/src/Tempest/View/src/Elements/ViewComponentElement.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\View\Elements;
6

7
use Dom\HTMLDocument;
8
use Tempest\Core\Environment;
9
use Tempest\View\Element;
10
use Tempest\View\Renderers\TempestViewCompiler;
11
use Tempest\View\Slot;
12
use Tempest\View\ViewComponent;
13

14
use function Tempest\Support\arr;
15
use function Tempest\Support\str;
16

17
use const Dom\HTML_NO_DEFAULT_NS;
18

19
final class ViewComponentElement implements Element
20
{
21
    use IsElement;
22

23
    private array $dataAttributes;
24

25
    public function __construct(
70✔
26
        private readonly Environment $environment,
27
        private readonly TempestViewCompiler $compiler,
28
        private readonly ViewComponent $viewComponent,
29
        array $attributes,
30
    ) {
31
        $this->attributes = $attributes;
70✔
32
        $this->dataAttributes = arr($attributes)
70✔
33
            ->filter(fn ($_, $key) => ! str_starts_with($key, ':'))
70✔
34
            // Attributes are converted to camelCase by default for PHP variable usage, but in the context of data attributes, kebab case is good
70✔
35
            ->mapWithKeys(fn ($value, $key) => yield str($key)->kebab()->toString() => $value)
70✔
36
            ->toArray();
70✔
37
    }
38

UNCOV
39
    public function getViewComponent(): ViewComponent
×
40
    {
UNCOV
41
        return $this->viewComponent;
×
42
    }
43

44
    /** @return Element[] */
45
    public function getSlots(): array
69✔
46
    {
47
        $slots = [];
69✔
48

49
        foreach ($this->getChildren() as $child) {
69✔
50
            if (! ($child instanceof SlotElement)) {
30✔
51
                continue;
28✔
52
            }
53

54
            $slots[] = $child;
6✔
55
        }
56

57
        return $slots;
69✔
58
    }
59

60
    public function getSlot(string $name = 'slot'): ?Element
33✔
61
    {
62
        foreach ($this->getChildren() as $child) {
33✔
63
            if (! ($child instanceof SlotElement)) {
29✔
64
                continue;
28✔
65
            }
66

67
            if ($child->matches($name)) {
5✔
68
                return $child;
3✔
69
            }
70
        }
71

72
        if ($name === 'slot') {
32✔
73
            $elements = [];
30✔
74

75
            foreach ($this->getChildren() as $child) {
30✔
76
                if ($child instanceof SlotElement) {
28✔
77
                    continue;
4✔
78
                }
79

80
                $elements[] = $child;
28✔
81
            }
82

83
            return new CollectionElement($elements);
30✔
84
        }
85

86
        return null;
2✔
87
    }
88

89
    public function compile(): string
69✔
90
    {
91
        /** @var Slot[] $slots */
92
        $slots = arr($this->getSlots())
69✔
93
            ->mapWithKeys(fn (SlotElement $element) => yield $element->name => Slot::fromElement($element))
69✔
94
            ->toArray();
69✔
95

96
        $compiled = str($this->viewComponent->compile($this))
69✔
97
            // Fallthrough attributes
69✔
98
            ->replaceRegex(
69✔
99
                regex: '/^<(?<tag>[\w-]+)(.*?["\s])?>/', // Match the very first opening tag, this will never fail.
69✔
100
                replace: function ($matches) {
69✔
101
                    $closingTag = '</' . $matches['tag'] . '>';
34✔
102

103
                    $html = $matches[0] . $closingTag;
34✔
104

105
                    $dom = HTMLDocument::createFromString($html, LIBXML_HTML_NOIMPLIED | LIBXML_NOERROR | HTML_NO_DEFAULT_NS);
34✔
106

107
                    /** @var \Dom\HTMLElement $element */
108
                    $element = $dom->childNodes[0];
34✔
109

110
                    foreach (['class', 'style', 'id'] as $attributeName) {
34✔
111
                        if (! isset($this->dataAttributes[$attributeName])) {
34✔
112
                            continue;
32✔
113
                        }
114

115
                        if ($attributeName === 'id') {
3✔
116
                            $value = $this->dataAttributes[$attributeName];
2✔
117
                        } else {
118
                            $value = arr([
3✔
119
                                $element->getAttribute($attributeName),
3✔
120
                                $this->dataAttributes[$attributeName],
3✔
121
                            ])
3✔
122
                                ->filter()
3✔
123
                                ->implode(' ')
3✔
124
                                ->toString();
3✔
125
                        }
126

127
                        $element->setAttribute(
3✔
128
                            qualifiedName: $attributeName,
3✔
129
                            value: $value,
3✔
130
                        );
3✔
131
                    }
132

133
                    return str($element->ownerDocument->saveHTML($element))->replaceLast($closingTag, '');
34✔
134
                },
69✔
135
            )
69✔
136
            ->prepend(
69✔
137
                // Add attributes to the current scope
138
                '<?php $_previousAttributes = $attributes ?? null; ?>',
69✔
139
                sprintf('<?php $attributes = \Tempest\Support\arr(%s); ?>', var_export($this->dataAttributes, true)), // @mago-expect best-practices/no-debug-symbols Set the new value of $attributes for this view component
69✔
140

141
                // Add dynamic slots to the current scope
142
                '<?php $_previousSlots = $slots ?? null; ?>', // Store previous slots in temporary variable to keep scope
69✔
143
                sprintf('<?php $slots = \Tempest\Support\arr(%s); ?>', var_export($slots, true)), // @mago-expect best-practices/no-debug-symbols Set the new value of $slots for this view component
69✔
144
            )
69✔
145
            ->append(
69✔
146
                // Restore previous slots
147
                '<?php unset($slots); ?>',
69✔
148
                '<?php $slots = $_previousSlots ?? null; ?>',
69✔
149
                '<?php unset($_previousSlots); ?>',
69✔
150

151
                // Restore previous attributes
152
                '<?php unset($attributes); ?>',
69✔
153
                '<?php $attributes = $_previousAttributes ?? null; ?>',
69✔
154
                '<?php unset($_previousAttributes); ?>',
69✔
155
            )
69✔
156
            // Compile slots
69✔
157
            ->replaceRegex(
69✔
158
                regex: '/<x-slot\s*(name="(?<name>\w+)")?((\s*\/>)|><\/x-slot>)/',
69✔
159
                replace: function ($matches) {
69✔
160
                    $name = $matches['name'] ?: 'slot';
33✔
161

162
                    $slot = $this->getSlot($name);
33✔
163

164
                    if ($slot === null) {
33✔
165
                        // A slot doesn't have any content, so we'll comment it out.
166
                        // This is to prevent DOM parsing errors (slots in <head> tags is one example, see #937)
167
                        return $this->environment->isProduction() ? '' : ('<!--' . $matches[0] . '-->');
2✔
168
                    }
169

170
                    return $slot->compile();
31✔
171
                },
69✔
172
            );
69✔
173

174
        return $this->compiler->compile($compiled->toString());
69✔
175
    }
176
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc