Skip to content

[TwigComponent] Store mount methods in compiler pass #2357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1872,7 +1872,10 @@ public function getTest(LiveComponentMetadataFactory $metadataFactory): Hydratio
return new HydrationTestCase(
$this->component,
new LiveComponentMetadata(
new ComponentMetadata(['key' => '__testing']),
new ComponentMetadata([
'key' => '__testing',
'mount' => $reflectionClass->hasMethod('mount') ? ['mount'] : [],
]),
$metadataFactory->createPropMetadatas($reflectionClass),
),
$this->inputProps,
Expand Down
40 changes: 0 additions & 40 deletions src/TwigComponent/src/Attribute/AsTwigComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,46 +75,6 @@ public function serviceConfig(): array
];
}

/**
* @param object|class-string $component
*
* @internal
*/
public static function mountMethod(object|string $component): ?\ReflectionMethod
{
foreach ((new \ReflectionClass($component))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if ('mount' === $method->getName()) {
return $method;
}
}

return null;
}

/**
* @param object|class-string $component
*
* @return \ReflectionMethod[]
*
* @internal
*/
public static function postMountMethods(object|string $component): array
{
return self::attributeMethodsByPriorityFor($component, PostMount::class);
}

/**
* @param object|class-string $component
*
* @return \ReflectionMethod[]
*
* @internal
*/
public static function preMountMethods(object|string $component): array
{
return self::attributeMethodsByPriorityFor($component, PreMount::class);
}

/**
* @param object|class-string $component
* @param class-string $attributeClass
Expand Down
18 changes: 9 additions & 9 deletions src/TwigComponent/src/Command/TwigComponentDebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentMetadata;
Expand Down Expand Up @@ -214,7 +213,7 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void
]);

// Anonymous Component
if (null === $metadata->get('class')) {
if ($metadata->isAnonymous()) {
$table->addRows([
['Type', '<comment>Anonymous</comment>'],
new TableSeparator(),
Expand All @@ -229,7 +228,7 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void
['Type', $metadata->get('live') ? '<info>Live</info>' : ''],
new TableSeparator(),
// ['Attributes Var', $metadata->get('attributes_var')],
['Public Props', $metadata->get('expose_public_props') ? 'Yes' : 'No'],
['Public Props', $metadata->isPublicPropsExposed() ? 'Yes' : 'No'],
['Properties', implode("\n", $this->getComponentProperties($metadata))],
]);

Expand All @@ -242,14 +241,15 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void
return \sprintf('%s(%s)', $m->getName(), implode(', ', $params));
};
$hooks = [];
if ($method = AsTwigComponent::mountMethod($metadata->getClass())) {
$hooks[] = ['Mount', $logMethod($method)];
$reflector = new \ReflectionClass($metadata->getClass());
foreach ($metadata->getPreMounts() as $method) {
$hooks[] = ['PreMount', $logMethod($reflector->getMethod($method))];
}
foreach (AsTwigComponent::preMountMethods($metadata->getClass()) as $method) {
$hooks[] = ['PreMount', $logMethod($method)];
foreach ($metadata->getMounts() as $method) {
$hooks[] = ['Mount', $logMethod($reflector->getMethod($method))];
}
foreach (AsTwigComponent::postMountMethods($metadata->getClass()) as $method) {
$hooks[] = ['PostMount', $logMethod($method)];
foreach ($metadata->getPostMounts() as $method) {
$hooks[] = ['PostMount', $logMethod($reflector->getMethod($method))];
}
if ($hooks) {
$table->addRows([
Expand Down
42 changes: 14 additions & 28 deletions src/TwigComponent/src/ComponentFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Contracts\Service\ResetInterface;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Event\PostMountEvent;
use Symfony\UX\TwigComponent\Event\PreMountEvent;

Expand All @@ -26,13 +25,12 @@
*/
final class ComponentFactory implements ResetInterface
{
private static $mountMethods = [];
private static $preMountMethods = [];
private static $postMountMethods = [];
private static array $mountMethods = [];

/**
* @param array<string, array> $config
* @param array<class-string, string> $classMap
* @param array<class-string, array<string, string[]> $classMounts
*/
public function __construct(
private ComponentTemplateFinderInterface $componentTemplateFinder,
Expand Down Expand Up @@ -92,7 +90,7 @@ public function mountFromObject(object $component, array $data, ComponentMetadat
$originalData = $data;
$data = $this->preMount($component, $data, $componentMetadata);

$this->mount($component, $data);
$this->mount($component, $data, $componentMetadata);

// set data that wasn't set in mount on the component directly
foreach ($data as $property => $value) {
Expand Down Expand Up @@ -144,42 +142,34 @@ public function get(string $name): object
return $this->components->get($metadata->getName());
}

private function mount(object $component, array &$data): void
private function mount(object $component, array &$data, ComponentMetadata $componentMetadata): void
{
if ($component instanceof AnonymousComponent) {
$component->mount($data);

return;
}

if (null === (self::$mountMethods[$component::class] ?? null)) {
try {
$mountMethod = self::$mountMethods[$component::class] = (new \ReflectionClass($component))->getMethod('mount');
} catch (\ReflectionException) {
self::$mountMethods[$component::class] = false;

return;
}
}

if (false === $mountMethod ??= self::$mountMethods[$component::class]) {
if (!$componentMetadata->getMounts()) {
return;
}

$mount = self::$mountMethods[$component::class] ??= (new \ReflectionClass($component))->getMethod('mount');

$parameters = [];
foreach ($mountMethod->getParameters() as $refParameter) {
foreach ($mount->getParameters() as $refParameter) {
if (\array_key_exists($name = $refParameter->getName(), $data)) {
$parameters[] = $data[$name];
// remove the data element so it isn't used to set the property directly.
unset($data[$name]);
} elseif ($refParameter->isDefaultValueAvailable()) {
$parameters[] = $refParameter->getDefaultValue();
} else {
throw new \LogicException(\sprintf('%s::mount() has a required $%s parameter. Make sure to pass it or give it a default value.', $component::class, $name));
throw new \LogicException(\sprintf('%s has a required $%s parameter. Make sure to pass it or give it a default value.', $component::class.'::mount()', $name));
}
}

$mountMethod->invoke($component, ...$parameters);
$mount->invoke($component, ...$parameters);
}

private function preMount(object $component, array $data, ComponentMetadata $componentMetadata): array
Expand All @@ -188,9 +178,8 @@ private function preMount(object $component, array $data, ComponentMetadata $com
$this->eventDispatcher->dispatch($event);
$data = $event->getData();

$methods = self::$preMountMethods[$component::class] ??= AsTwigComponent::preMountMethods($component::class);
foreach ($methods as $method) {
if (null !== $newData = $method->invoke($component, $data)) {
foreach ($componentMetadata->getPreMounts() as $preMount) {
if (null !== $newData = $component->$preMount($data)) {
$data = $newData;
}
}
Expand All @@ -207,9 +196,8 @@ private function postMount(object $component, array $data, ComponentMetadata $co
$this->eventDispatcher->dispatch($event);
$data = $event->getData();

$methods = self::$postMountMethods[$component::class] ??= AsTwigComponent::postMountMethods($component::class);
foreach ($methods as $method) {
if (null !== $newData = $method->invoke($component, $data)) {
foreach ($componentMetadata->getPostMounts() as $postMount) {
if (null !== $newData = $component->$postMount($data)) {
$data = $newData;
}
}
Expand Down Expand Up @@ -257,7 +245,5 @@ private function throwUnknownComponentException(string $name): void
public function reset(): void
{
self::$mountMethods = [];
self::$preMountMethods = [];
self::$postMountMethods = [];
}
}
30 changes: 30 additions & 0 deletions src/TwigComponent/src/ComponentMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,36 @@ public function getAttributesVar(): string
return $this->get('attributes_var', 'attributes');
}

/**
* @return list<string>
*
* @internal
*/
public function getPreMounts(): array
{
return $this->get('pre_mount', []);
}

/**
* @return list<string>
*
* @internal
*/
public function getMounts(): array
{
return $this->get('mount', []);
}

/**
* @return list<string>
*
* @internal
*/
public function getPostMounts(): array
{
return $this->get('post_mount', []);
}

public function get(string $key, mixed $default = null): mixed
{
return $this->config[$key] ?? $default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\UX\TwigComponent\Attribute\PostMount;
use Symfony\UX\TwigComponent\Attribute\PreMount;

/**
* @author Kevin Bond <kevinbond@gmail.com>
Expand Down Expand Up @@ -68,7 +70,7 @@ public function process(ContainerBuilder $container): void
$tag['service_id'] = $id;
$tag['class'] = $definition->getClass();
$tag['template'] = $tag['template'] ?? $this->calculateTemplate($tag['key'], $defaults);
$componentConfig[$tag['key']] = $tag;
$componentConfig[$tag['key']] = [...$tag, ...$this->getMountMethods($tag['class'])];
$componentReferences[$tag['key']] = new Reference($id);
$componentNames[] = $tag['key'];
$componentClassMap[$tag['class']] = $tag['key'];
Expand Down Expand Up @@ -109,4 +111,35 @@ private function calculateTemplate(string $componentName, ?array $defaults): str

return \sprintf('%s/%s.html.twig', rtrim($directory, '/'), str_replace(':', '/', $componentName));
}

/**
* @param class-string $component
*
* @return array{preMount: string[], mount: string[], postMount: string[]}
*/
private function getMountMethods(string $component): array
{
$preMount = $mount = $postMount = [];
foreach ((new \ReflectionClass($component))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
foreach ($method->getAttributes(PreMount::class) as $attribute) {
$preMount[$method->getName()] = $attribute->newInstance()->priority;
}
foreach ($method->getAttributes(PostMount::class) as $attribute) {
$postMount[$method->getName()] = $attribute->newInstance()->priority;
}
if ('mount' === $method->getName()) {
$mount['mount'] = 0;
}
}

arsort($preMount, \SORT_NUMERIC);
arsort($mount, \SORT_NUMERIC);
arsort($postMount, \SORT_NUMERIC);

return [
'pre_mount' => array_keys($preMount),
'mount' => array_keys($mount),
'post_mount' => array_keys($postMount),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@
#[AsTwigComponent]
class AcmeRootComponent
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@
#[AsTwigComponent]
class AcmeOtherComponent
{

}
2 changes: 1 addition & 1 deletion src/TwigComponent/tests/Fixtures/Component/Conflict.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
class Conflict
{
public string $name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@
#[AsTwigComponent]
class ComponentInSubDirectory
{

}
12 changes: 8 additions & 4 deletions src/TwigComponent/tests/Integration/ComponentExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ public function testRenderingComponentWithNestedAttributes(): void
{
$output = $this->renderComponent('NestedAttributes');

$this->assertSame(<<<HTML
$this->assertSame(
<<<HTML
<main>
<div>
<span>
Expand All @@ -302,7 +303,8 @@ public function testRenderingComponentWithNestedAttributes(): void
'title:span:class' => 'baz',
]);

$this->assertSame(<<<HTML
$this->assertSame(
<<<HTML
<main class="foo">
<div class="bar">
<span class="baz">
Expand All @@ -324,7 +326,8 @@ public function testRenderingHtmlSyntaxComponentWithNestedAttributes(): void
->render()
;

$this->assertSame(<<<HTML
$this->assertSame(
<<<HTML
<main>
<div>
<span>
Expand All @@ -343,7 +346,8 @@ public function testRenderingHtmlSyntaxComponentWithNestedAttributes(): void
->render()
;

$this->assertSame(<<<HTML
$this->assertSame(
<<<HTML
<main class="foo" @class="vex">
<div class="bar">
<span class="baz">
Expand Down
Loading