Skip to content

Commit c2f7738

Browse files
committed
bug #2749 Remove ComponentAttributeFactory and inject EscaperRuntime directly (smnandre)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- Remove ComponentAttributeFactory and inject EscaperRuntime directly Replaces the `ComponentAttributesFactory` with direct usage of the `EscaperRuntime` from Twig ### Inject `EscaperRuntime` in `ComponentAttributes` * Replaced `ComponentAttributesFactory` with `Twig\Environment` and `EscaperRuntime` in `AddLiveAttributesSubscriber`, `LiveComponentHydrator`, and `ChildComponentPartialRenderer`, ensuring `ComponentAttributes` is instantiated with the necessary `EscaperRuntime`. * Updated the `ComponentAttributes` constructor to require `EscaperRuntime` directly, removing the deprecated `HtmlAttributeEscaperInterface`. ### Remove `ComponentAttributeFactory` * Removed all references to `ComponentAttributesFactory` and its related logic across the codebase * Adjusted unit and integration tests to reflect the removal of `ComponentAttributesFactory` ### Dependency Updates * Updated `symfony/ux-twig-component` dependency in `composer.json` to version `^2.25.1` ### Documentation Updates * Added a new entry in the `CHANGELOG.md` for version `2.25.1` Commits ------- 13755d9 Remove ComponentAttributeFactory and inject EscaperRuntime directly
2 parents 4e113bb + 13755d9 commit c2f7738

19 files changed

+143
-361
lines changed

src/LiveComponent/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"symfony/property-access": "^5.4.5|^6.0|^7.0",
3232
"symfony/property-info": "^5.4|^6.0|^7.0",
3333
"symfony/stimulus-bundle": "^2.9",
34-
"symfony/ux-twig-component": "^2.25",
34+
"symfony/ux-twig-component": "^2.25.1",
3535
"twig/twig": "^3.10.3"
3636
},
3737
"require-dev": {

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
1616
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1717
use Symfony\Component\Config\Definition\ConfigurationInterface;
18-
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
1918
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
2019
use Symfony\Component\DependencyInjection\ChildDefinition;
2120
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -110,7 +109,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
110109
new Reference('ux.live_component.metadata_factory'),
111110
new Reference('serializer', ContainerInterface::NULL_ON_INVALID_REFERENCE),
112111
$config['secret'], // defaults to '%kernel.secret%'
113-
new Reference('ux.twig_component.component_attributes_factory'),
112+
new Reference('twig'),
114113
])
115114
;
116115

@@ -158,7 +157,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
158157
->setArguments([
159158
new Reference('ux.live_component.fingerprint_calculator'),
160159
new Reference('ux.live_component.attribute_helper_factory'),
161-
new Reference('ux.twig_component.component_attributes_factory'),
160+
new Reference('twig'),
162161
])
163162
->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory'])
164163
->addTag('container.service_subscriber', ['key' => LiveComponentMetadataFactory::class, 'id' => 'ux.live_component.metadata_factory'])
@@ -219,7 +218,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
219218
->setArguments([
220219
new Reference('ux.twig_component.component_stack'),
221220
new Reference('ux.live_component.twig.template_mapper'),
222-
new Reference('ux.twig_component.component_attributes_factory'),
221+
new Reference('twig'),
223222
])
224223
->addTag('kernel.event_subscriber')
225224
->addTag('container.service_subscriber', ['key' => LiveControllerAttributesCreator::class, 'id' => 'ux.live_component.live_controller_attributes_creator'])

src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
use Symfony\UX\LiveComponent\Twig\TemplateMap;
1818
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
1919
use Symfony\UX\TwigComponent\ComponentAttributes;
20-
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
2120
use Symfony\UX\TwigComponent\ComponentMetadata;
2221
use Symfony\UX\TwigComponent\ComponentStack;
2322
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
2423
use Symfony\UX\TwigComponent\MountedComponent;
24+
use Twig\Environment;
25+
use Twig\Runtime\EscaperRuntime;
2526

2627
/**
2728
* Adds the extra attributes needed to activate a live controller.
@@ -37,7 +38,7 @@ final class AddLiveAttributesSubscriber implements EventSubscriberInterface, Ser
3738
public function __construct(
3839
private ComponentStack $componentStack,
3940
private TemplateMap $templateMap,
40-
private readonly ComponentAttributesFactory $componentAttributesFactory,
41+
private readonly Environment $twig,
4142
private ContainerInterface $container,
4243
) {
4344
}
@@ -107,6 +108,6 @@ private function getLiveAttributes(MountedComponent $mounted, ComponentMetadata
107108
$this->componentStack->hasParentComponent()
108109
);
109110

110-
return $this->componentAttributesFactory->create($attributesCollection->toArray());
111+
return new ComponentAttributes($attributesCollection->toArray(), $this->twig->getRuntime(EscaperRuntime::class));
111112
}
112113
}

src/LiveComponent/src/LiveComponentHydrator.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
use Symfony\UX\LiveComponent\Metadata\LivePropMetadata;
3333
use Symfony\UX\LiveComponent\Util\DehydratedProps;
3434
use Symfony\UX\TwigComponent\ComponentAttributes;
35-
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
35+
use Twig\Environment;
36+
use Twig\Runtime\EscaperRuntime;
3637

3738
/**
3839
* @author Kevin Bond <kevinbond@gmail.com>
@@ -53,7 +54,7 @@ public function __construct(
5354
private LiveComponentMetadataFactory $liveComponentMetadataFactory,
5455
private NormalizerInterface|DenormalizerInterface|null $serializer,
5556
#[\SensitiveParameter] private string $secret,
56-
private readonly ComponentAttributesFactory $componentAttributesFactory,
57+
private readonly Environment $twig,
5758
) {
5859
if (!$secret) {
5960
throw new \InvalidArgumentException('A non-empty secret is required.');
@@ -146,7 +147,7 @@ public function hydrate(object $component, array $props, array $updatedProps, Li
146147
$dehydratedOriginalProps = $this->combineAndValidateProps($props, $updatedPropsFromParent);
147148
$dehydratedUpdatedProps = DehydratedProps::createFromUpdatedArray($updatedProps);
148149

149-
$attributes = $this->componentAttributesFactory->create($dehydratedOriginalProps->getPropValue(self::ATTRIBUTES_KEY, []));
150+
$attributes = new ComponentAttributes($dehydratedOriginalProps->getPropValue(self::ATTRIBUTES_KEY, []), $this->twig->getRuntime(EscaperRuntime::class));
150151
$dehydratedOriginalProps->removePropValue(self::ATTRIBUTES_KEY);
151152

152153
$needProcessOnUpdatedHooks = [];

src/LiveComponent/src/Util/ChildComponentPartialRenderer.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
use Symfony\Contracts\Service\ServiceSubscriberInterface;
1616
use Symfony\UX\LiveComponent\LiveComponentHydrator;
1717
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
18-
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
18+
use Symfony\UX\TwigComponent\ComponentAttributes;
1919
use Symfony\UX\TwigComponent\ComponentFactory;
20+
use Twig\Environment;
21+
use Twig\Runtime\EscaperRuntime;
2022

2123
/**
2224
* @author Ryan Weaver <ryan@symfonycasts.com>
@@ -28,7 +30,7 @@ class ChildComponentPartialRenderer implements ServiceSubscriberInterface
2830
public function __construct(
2931
private FingerprintCalculator $fingerprintCalculator,
3032
private TwigAttributeHelperFactory $attributeHelperFactory,
31-
private ComponentAttributesFactory $componentAttributesFactory,
33+
private Environment $twig,
3234
private ContainerInterface $container,
3335
) {
3436
}
@@ -85,7 +87,7 @@ public function renderChildComponent(string $deterministicId, string $currentPro
8587
private function createHtml(array $attributes, string $childTag): string
8688
{
8789
$attributes['data-live-preserve'] = true;
88-
$attributes = $this->componentAttributesFactory->create($attributes);
90+
$attributes = new ComponentAttributes($attributes, $this->twig->getRuntime(EscaperRuntime::class));
8991

9092
return \sprintf('<%s%s></%s>', $childTag, $attributes, $childTag);
9193
}

src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@
4343
use Symfony\UX\LiveComponent\Tests\Fixtures\Enum\ZeroIntEnum;
4444
use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
4545
use Symfony\UX\TwigComponent\ComponentAttributes;
46-
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
4746
use Symfony\UX\TwigComponent\ComponentMetadata;
47+
use Twig\Environment;
48+
use Twig\Runtime\EscaperRuntime;
4849
use Zenstruck\Foundry\Test\Factories;
4950
use Zenstruck\Foundry\Test\ResetDatabase;
5051

@@ -76,9 +77,9 @@ private function executeHydrationTestCase(callable $testFactory, ?int $minPhpVer
7677
$metadataFactory = self::getContainer()->get('ux.live_component.metadata_factory');
7778
\assert($metadataFactory instanceof LiveComponentMetadataFactory);
7879
$testCase = $testBuilder->getTest($metadataFactory);
79-
80-
$componentAttributesFactory = self::getContainer()->get('ux.twig_component.component_attributes_factory');
81-
\assert($componentAttributesFactory instanceof ComponentAttributesFactory);
80+
81+
$twig = self::getContainer()->get('twig');
82+
\assert($twig instanceof Environment);
8283

8384
// keep a copy of the original, empty component object for hydration later
8485
$originalComponentWithData = clone $testCase->component;
@@ -94,7 +95,7 @@ private function executeHydrationTestCase(callable $testFactory, ?int $minPhpVer
9495

9596
$dehydratedProps = $this->hydrator()->dehydrate(
9697
$originalComponentWithData,
97-
$componentAttributesFactory->create([]), // not worried about testing these here
98+
new ComponentAttributes([], $twig->getRuntime(EscaperRuntime::class)), // not worried about testing these here
9899
$liveMetadata,
99100
);
100101

@@ -136,7 +137,7 @@ private function executeHydrationTestCase(callable $testFactory, ?int $minPhpVer
136137

137138
$dehydratedProps2 = $this->hydrator()->dehydrate(
138139
$componentAfterHydration,
139-
$componentAttributesFactory->create(),
140+
new ComponentAttributes([], $twig->getRuntime(EscaperRuntime::class)),
140141
$liveMetadata,
141142
);
142143
$this->hydrator()->hydrate(
@@ -1824,14 +1825,14 @@ public static function falseyValueProvider(): iterable
18241825
yield ['nullableBool', '', null];
18251826
yield 'fooey-o-booey-todo' => ['nullableBool', ' ', null];
18261827
}
1827-
1828+
18281829
private function createComponentAttributes(array $attributes = []): ComponentAttributes
18291830
{
1830-
$factory = self::getContainer()->get('ux.twig_component.component_attributes_factory');
1831-
\assert($factory instanceof ComponentAttributesFactory);
1832-
1833-
return $factory->create($attributes);
1834-
}
1831+
$twig = self::getContainer()->get('twig');
1832+
\assert($twig instanceof Environment);
1833+
1834+
return new ComponentAttributes($attributes, $twig->getRuntime(EscaperRuntime::class));
1835+
}
18351836

18361837
private function createLiveMetadata(object $component): LiveComponentMetadata
18371838
{

src/LiveComponent/tests/Unit/LiveComponentHydratorTest.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
use Symfony\UX\LiveComponent\LiveComponentHydrator;
2121
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
2222
use Symfony\UX\LiveComponent\Metadata\LivePropMetadata;
23-
use Symfony\UX\TwigComponent\ComponentAttributesFactory;
2423
use Twig\Environment;
2524

2625
final class LiveComponentHydratorTest extends TestCase
@@ -36,7 +35,7 @@ public function testConstructWithEmptySecret(): void
3635
$this->createMock(LiveComponentMetadataFactory::class),
3736
$this->createMock(NormalizerInterface::class),
3837
'',
39-
new ComponentAttributesFactory($this->createMock(Environment::class)),
38+
$this->createMock(Environment::class),
4039
);
4140
}
4241

@@ -48,7 +47,7 @@ public function testItCanHydrateWithNullValues()
4847
$this->createMock(LiveComponentMetadataFactory::class),
4948
new Serializer(normalizers: [new ObjectNormalizer()]),
5049
'foo',
51-
new ComponentAttributesFactory($this->createMock(Environment::class)),
50+
$this->createMock(Environment::class),
5251
);
5352

5453
$hydratedValue = $hydrator->hydrateValue(

src/TwigComponent/CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# CHANGELOG
22

3+
## 2.25.1
4+
5+
- [SECURITY] `ComponentAttributes` now requires a `Twig\Runtime\EscaperRuntime`
6+
instance as second argument
7+
- Remove `HtmlAttributeEscaperInterface`, `TwigHtmlAttributeEscaper` and `ComponentAttributesFactory`
8+
39
## 2.25.0
410

511
- [SECURITY] Make `ComponentAttributes` responsible for attribute escaping ensuring
6-
consistent and secure HTML output across all rendering contexts.
12+
consistent and secure HTML output across all rendering contexts
713
- Deprecate not passing an `HtmlAttributeEscaperInterface` to the `ComponentAttributes`
8-
constructor.
14+
constructor
915

1016
## 2.20.0
1117

src/TwigComponent/src/ComponentAttributes.php

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
namespace Symfony\UX\TwigComponent;
1313

1414
use Symfony\UX\StimulusBundle\Dto\StimulusAttributes;
15-
use Symfony\UX\TwigComponent\Escaper\HtmlAttributeEscaperInterface;
1615
use Symfony\WebpackEncoreBundle\Dto\AbstractStimulusDto;
16+
use Twig\Runtime\EscaperRuntime;
1717

1818
/**
1919
* @author Kevin Bond <kevinbond@gmail.com>
@@ -29,19 +29,13 @@ final class ComponentAttributes implements \Stringable, \IteratorAggregate, \Cou
2929
/** @var array<string,true> */
3030
private array $rendered = [];
3131

32-
private readonly ?HtmlAttributeEscaperInterface $escaper;
33-
3432
/**
3533
* @param array<string, string|bool> $attributes
3634
*/
3735
public function __construct(
3836
private array $attributes,
39-
?HtmlAttributeEscaperInterface $escaper = null,
37+
private readonly EscaperRuntime $escaper,
4038
) {
41-
// Third argument used as internal flag to prevent multiple deprecations
42-
if ((null === $this->escaper = $escaper) && 3 > func_num_args()) {
43-
trigger_deprecation('symfony/ux-twig-component', '2.24', 'Not passing an "%s" to "%s" is deprecated and will throw in 3.0.', HtmlAttributeEscaperInterface::class, self::class);
44-
}
4539
}
4640

4741
public function __toString(): string
@@ -87,13 +81,17 @@ public function __toString(): string
8781
// - special syntax names (Vue.js, Svelte, Alpine.js, ...)
8882
// v-*, x-*, @*, :*
8983
if (!ctype_alpha(str_replace(['-', '_', ':', '@', '.'], '', $key))) {
90-
$key = $this->escaper?->escapeName($key) ?? $key;
84+
$key = (string) $this->escaper->escape($key, 'html_attr');
9185
}
9286

9387
if (true === $value) {
9488
$attributes .= ' '.$key;
9589
} else {
96-
$attributes .= ' '.\sprintf('%s="%s"', $key, $this->escaper?->escapeValue($value) ?? $value);
90+
if (!ctype_alnum(str_replace(['-', '_'], '', $value))) {
91+
$value = $this->escaper->escape($value, 'html');
92+
}
93+
94+
$attributes .= ' '.\sprintf('%s="%s"', $key, $value);
9795
}
9896
}
9997

@@ -167,7 +165,7 @@ public function defaults(iterable $attributes): self
167165
unset($attributes[$attribute]);
168166
}
169167

170-
return new self($attributes, $this->escaper, true);
168+
return new self($attributes, $this->escaper);
171169
}
172170

173171
/**
@@ -183,7 +181,7 @@ public function only(string ...$keys): self
183181
}
184182
}
185183

186-
return new self($attributes, $this->escaper, true);
184+
return new self($attributes, $this->escaper);
187185
}
188186

189187
/**
@@ -221,7 +219,7 @@ public function add($stimulusDto): self
221219
)));
222220
unset($controllersAttributes['data-controller']);
223221

224-
$clone = new self($attributes, $this->escaper, true);
222+
$clone = new self($attributes, $this->escaper);
225223

226224
// add the remaining attributes for values/classes
227225
return $clone->defaults($controllersAttributes);
@@ -233,7 +231,7 @@ public function remove($key): self
233231

234232
unset($attributes[$key]);
235233

236-
return new self($attributes, $this->escaper, true);
234+
return new self($attributes, $this->escaper);
237235
}
238236

239237
public function nested(string $namespace): self
@@ -249,7 +247,7 @@ public function nested(string $namespace): self
249247
}
250248
}
251249

252-
return new self($attributes, $this->escaper, true);
250+
return new self($attributes, $this->escaper);
253251
}
254252

255253
public function getIterator(): \Traversable

src/TwigComponent/src/ComponentAttributesFactory.php

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)