Skip to content

Commit 387310a

Browse files
committed
Add a ComponentProperties service to reuse computation
First version to replicate behaviour + some code fixes In an IRL app, the number of class-based components is not something "big". So this could/should be computed once during warm up. Next steps: - adapt DI - introduce cache + cachewarmer
1 parent 5944258 commit 387310a

File tree

2 files changed

+130
-57
lines changed

2 files changed

+130
-57
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\UX\TwigComponent;
13+
14+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
15+
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
16+
17+
/**
18+
* @author Simon André <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
final class ComponentProperties
23+
{
24+
private array $classMetadata = [];
25+
26+
public function __construct(
27+
private readonly PropertyAccessorInterface $propertyAccessor,
28+
) {
29+
}
30+
31+
/**
32+
* @return array<string, mixed>
33+
*/
34+
public function getProperties(object $component, bool $publicProps = false): array
35+
{
36+
return iterator_to_array($this->extractProperties($component, $publicProps));
37+
}
38+
39+
private function extractProperties(object $component, bool $publicProps): iterable
40+
{
41+
yield from $publicProps ? get_object_vars($component) : [];
42+
43+
$metadata = $this->classMetadata[$component::class] ??= $this->loadClassMetadata($component::class);
44+
45+
foreach ($metadata['properties'] as $propertyName => $property) {
46+
$value = $property['getter'] ? $component->{$property['getter']}() : $this->propertyAccessor->getValue($component, $propertyName);
47+
if ($property['destruct'] ?? false) {
48+
yield from $value;
49+
} else {
50+
yield $property['name'] => $value;
51+
}
52+
}
53+
54+
foreach ($metadata['methods'] as $methodName => $method) {
55+
if ($method['destruct'] ?? false) {
56+
yield from $component->{$methodName}();
57+
} else {
58+
yield $method['name'] => $component->{$methodName}();
59+
}
60+
}
61+
}
62+
63+
/**
64+
* @param class-string $class
65+
*
66+
* @return array{
67+
* properties: array<string, array{
68+
* name?: string,
69+
* getter?: string,
70+
* destruct?: bool
71+
* }>,
72+
* methods: array<string, array{
73+
* name?: string,
74+
* destruct?: bool
75+
* }>,
76+
* }
77+
*/
78+
private function loadClassMetadata(string $class): array
79+
{
80+
$refClass = new \ReflectionClass($class);
81+
82+
$properties = [];
83+
foreach ($refClass->getProperties() as $property) {
84+
if (!$attributes = $property->getAttributes(ExposeInTemplate::class)) {
85+
continue;
86+
}
87+
$attribute = $attributes[0]->newInstance();
88+
89+
$properties[$property->name] = [
90+
'name' => $attribute->name ?? $property->name,
91+
'getter' => $attribute->getter ? rtrim($attribute->getter, '()') : null,
92+
];
93+
94+
if ($attribute->destruct) {
95+
unset($properties[$property->name]['name']);
96+
$properties[$property->name]['destruct'] = true;
97+
}
98+
}
99+
100+
$methods = [];
101+
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
102+
if (!$attributes = $method->getAttributes(ExposeInTemplate::class)) {
103+
continue;
104+
}
105+
if ($method->getNumberOfRequiredParameters()) {
106+
throw new \LogicException(\sprintf('Cannot use "%s" on methods with required parameters (%s::%s).', ExposeInTemplate::class, $class, $method->name));
107+
}
108+
109+
$attribute = $attributes[0]->newInstance();
110+
111+
$name = $attribute->name ?? (str_starts_with($method->name, 'get') ? lcfirst(substr($method->name, 3)) : $method->name);
112+
113+
$methods[$method->name] = $attribute->destruct ? ['destruct' => true] : ['name' => $name];
114+
}
115+
116+
return [
117+
'properties' => $properties,
118+
'methods' => $methods,
119+
];
120+
}
121+
}

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 9 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1515
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
16-
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
1716
use Symfony\UX\TwigComponent\Event\PostRenderEvent;
1817
use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
1918
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
@@ -26,13 +25,17 @@
2625
*/
2726
final class ComponentRenderer implements ComponentRendererInterface
2827
{
28+
// TODO update DI
29+
private readonly ComponentProperties $componentProperties;
30+
2931
public function __construct(
3032
private Environment $twig,
3133
private EventDispatcherInterface $dispatcher,
3234
private ComponentFactory $factory,
33-
private PropertyAccessorInterface $propertyAccessor,
35+
PropertyAccessorInterface $propertyAccessor,
3436
private ComponentStack $componentStack,
3537
) {
38+
$this->componentProperties = new ComponentProperties($propertyAccessor);
3639
}
3740

3841
/**
@@ -107,9 +110,11 @@ private function preRender(MountedComponent $mounted, array $context = []): PreR
107110
{
108111
$component = $mounted->getComponent();
109112
$metadata = $this->factory->metadataFor($mounted->getName());
110-
$isAnonymous = $mounted->getComponent() instanceof AnonymousComponent;
111113

112-
$classProps = $isAnonymous ? [] : iterator_to_array($this->exposedVariables($component, $metadata->isPublicPropsExposed()));
114+
$classProps = [];
115+
if (!$metadata->isAnonymous()) {
116+
$classProps = $this->componentProperties->getProperties($component, $metadata->isPublicPropsExposed());
117+
}
113118

114119
// expose public properties and properties marked with ExposeInTemplate attribute
115120
$props = [...$mounted->getInputProps(), ...$classProps];
@@ -137,57 +142,4 @@ private function preRender(MountedComponent $mounted, array $context = []): PreR
137142

138143
return $event;
139144
}
140-
141-
private function exposedVariables(object $component, bool $exposePublicProps): \Iterator
142-
{
143-
if ($exposePublicProps) {
144-
yield from get_object_vars($component);
145-
}
146-
147-
$class = new \ReflectionClass($component);
148-
149-
foreach ($class->getProperties() as $property) {
150-
if (!$attribute = $property->getAttributes(ExposeInTemplate::class)[0] ?? null) {
151-
continue;
152-
}
153-
154-
$attribute = $attribute->newInstance();
155-
156-
/** @var ExposeInTemplate $attribute */
157-
$value = $attribute->getter ? $component->{rtrim($attribute->getter, '()')}() : $this->propertyAccessor->getValue($component, $property->name);
158-
159-
if ($attribute->destruct) {
160-
foreach ($value as $key => $destructedValue) {
161-
yield $key => $destructedValue;
162-
}
163-
}
164-
165-
yield $attribute->name ?? $property->name => $value;
166-
}
167-
168-
foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
169-
if (!$attribute = $method->getAttributes(ExposeInTemplate::class)[0] ?? null) {
170-
continue;
171-
}
172-
173-
$attribute = $attribute->newInstance();
174-
175-
/** @var ExposeInTemplate $attribute */
176-
$name = $attribute->name ?? (str_starts_with($method->name, 'get') ? lcfirst(substr($method->name, 3)) : $method->name);
177-
178-
if ($method->getNumberOfRequiredParameters()) {
179-
throw new \LogicException(\sprintf('Cannot use "%s" on methods with required parameters (%s::%s).', ExposeInTemplate::class, $component::class, $method->name));
180-
}
181-
182-
if ($attribute->destruct) {
183-
foreach ($component->{$method->name}() as $prop => $value) {
184-
yield $prop => $value;
185-
}
186-
187-
return;
188-
}
189-
190-
yield $name => $component->{$method->name}();
191-
}
192-
}
193145
}

0 commit comments

Comments
 (0)