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

tempestphp / tempest-framework / 14654722276

24 Apr 2025 07:39PM UTC coverage: 80.219% (+0.03%) from 80.194%
14654722276

push

github

web-flow
fix(view): wrong matched imports in view component slots (#1173)

Co-authored-by: brendt <brent.roose@gmail.com>

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

2 existing lines in 2 files now uncovered.

11785 of 14691 relevant lines covered (80.22%)

106.33 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Tempest\View\Elements;
6

7
use Tempest\Core\Environment;
8
use Tempest\Support\Str\ImmutableString;
9
use Tempest\Support\Str\MutableString;
10
use Tempest\View\Element;
11
use Tempest\View\Parser\TempestViewCompiler;
12
use Tempest\View\Parser\TempestViewParser;
13
use Tempest\View\Slot;
14
use Tempest\View\ViewComponent;
15

16
use function Tempest\Support\arr;
17
use function Tempest\Support\str;
18

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

23
    private array $dataAttributes;
24

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

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

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

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

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

57
        return $slots;
82✔
58
    }
59

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

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

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

75
            foreach ($this->getChildren() as $child) {
34✔
76
                if ($child instanceof SlotElement) {
32✔
77
                    continue;
5✔
78
                }
79

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

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

86
        return null;
2✔
87
    }
88

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

96
        $compiled = str($this->viewComponent->compile($this));
82✔
97

98
        $compiled = $compiled
82✔
99
            // Fallthrough attributes
82✔
100
            ->replaceRegex(
82✔
101
                regex: '/^<(?<tag>[\w-]+)(.*?["\s])?>/', // Match the very first opening tag, this will never fail.
82✔
102
                replace: function ($matches) {
82✔
103
                    /** @var \Tempest\View\Parser\Token $token */
104
                    $token = TempestViewParser::ast($matches[0])[0];
41✔
105

106
                    $attributes = arr($token->htmlAttributes)->map(fn (string $value) => new MutableString($value));
41✔
107

108
                    foreach (['class', 'style', 'id'] as $attributeName) {
41✔
109
                        if (! isset($this->dataAttributes[$attributeName])) {
41✔
110
                            continue;
38✔
111
                        }
112

113
                        $attributes[$attributeName] ??= new MutableString();
3✔
114

115
                        if ($attributeName === 'id') {
3✔
116
                            $attributes[$attributeName] = new MutableString(' ' . $this->dataAttributes[$attributeName]);
3✔
117
                        } else {
118
                            $attributes[$attributeName]->append(' ' . $this->dataAttributes[$attributeName]);
3✔
119
                        }
120
                    }
121

122
                    return sprintf(
41✔
123
                        '<%s%s>',
41✔
124
                        $matches['tag'],
41✔
125
                        $attributes
41✔
126
                            ->map(function (MutableString $value, string $key) {
41✔
127
                                return sprintf('%s="%s"', $key, $value->trim());
20✔
128
                            })
41✔
129
                            ->implode(' ')
41✔
130
                            ->when(
41✔
131
                                fn (ImmutableString $string) => $string->isNotEmpty(),
41✔
132
                                fn (ImmutableString $string) => $string->prepend(' '),
41✔
133
                            ),
41✔
134
                    );
41✔
135
                },
82✔
136
            );
82✔
137

138
        $compiled = $compiled
82✔
139
            ->prepend(
82✔
140
                // Add attributes to the current scope
141
                '<?php $_previousAttributes = $attributes ?? null; ?>',
82✔
142
                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
82✔
143

144
                // Add dynamic slots to the current scope
145
                '<?php $_previousSlots = $slots ?? null; ?>', // Store previous slots in temporary variable to keep scope
82✔
146
                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
82✔
147
            )
82✔
148
            ->append(
82✔
149
                // Restore previous slots
150
                '<?php unset($slots); ?>',
82✔
151
                '<?php $slots = $_previousSlots ?? null; ?>',
82✔
152
                '<?php unset($_previousSlots); ?>',
82✔
153

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

165
                    $slot = $this->getSlot($name);
40✔
166

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

173
                    return $slot->compile();
38✔
174
                },
82✔
175
            );
82✔
176

177
        return $this->compiler->compile($compiled->toString());
82✔
178
    }
179
}
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

© 2025 Coveralls, Inc