Skip to content

Commit

Permalink
minor #2201 [TwigComponent] Optimize ComponentFactory (smnandre)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[TwigComponent] Optimize ComponentFactory

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Issues        | Fix #...
| License       | MIT

Some internal refactor focused on the Component Factory / Anonymous components usage.

* Optimize the hot path
* Store anonymous template resolution
* Avoid anonymous checks for class-based components
* Add ComponentFactory unit tests
* Add ComponentMetadata::isAnonymous() method
* Fix loop
* Reuse metadata to instanciate and mount component

Other PRs will follow :)

Commits
-------

da0537d [TwigComponent] Optimize ComponentFactory
  • Loading branch information
javiereguiluz committed Sep 24, 2024
2 parents 06e026c + da0537d commit c7e4532
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 39 deletions.
73 changes: 34 additions & 39 deletions src/TwigComponent/src/ComponentFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,42 @@ public function __construct(

public function metadataFor(string $name): ComponentMetadata
{
$name = $this->classMap[$name] ?? $name;

if (!$config = $this->config[$name] ?? null) {
if (($template = $this->componentTemplateFinder->findAnonymousComponentTemplate($name)) !== null) {
return new ComponentMetadata([
'key' => $name,
'template' => $template,
]);
if ($config = $this->config[$name] ?? null) {
return new ComponentMetadata($config);
}

if ($template = $this->componentTemplateFinder->findAnonymousComponentTemplate($name)) {
$this->config[$name] = [
'key' => $name,
'template' => $template,
];

return new ComponentMetadata($this->config[$name]);
}

if ($mappedName = $this->classMap[$name] ?? null) {
if ($config = $this->config[$mappedName] ?? null) {
return new ComponentMetadata($config);
}

$this->throwUnknownComponentException($name);
throw new \InvalidArgumentException(\sprintf('Unknown component "%s".', $name));
}

return new ComponentMetadata($config);
$this->throwUnknownComponentException($name);
}

/**
* Creates the component and "mounts" it with the passed data.
*/
public function create(string $name, array $data = []): MountedComponent
{
return $this->mountFromObject(
$this->getComponent($name),
$data,
$this->metadataFor($name)
);
$metadata = $this->metadataFor($name);

if ($metadata->isAnonymous()) {
return $this->mountFromObject(new AnonymousComponent(), $data, $metadata);
}

return $this->mountFromObject($this->components->get($metadata->getName()), $data, $metadata);
}

/**
Expand Down Expand Up @@ -101,10 +111,7 @@ public function mountFromObject(object $component, array $data, ComponentMetadat
foreach ($data as $key => $value) {
if ($value instanceof \Stringable) {
$data[$key] = (string) $value;
continue;
}

$data[$key] = $value;
}

return new MountedComponent(
Expand All @@ -118,10 +125,18 @@ public function mountFromObject(object $component, array $data, ComponentMetadat

/**
* Returns the "unmounted" component.
*
* @internal
*/
public function get(string $name): object
{
return $this->getComponent($name);
$metadata = $this->metadataFor($name);

if ($metadata->isAnonymous()) {
return new AnonymousComponent();
}

return $this->components->get($metadata->getName());
}

private function mount(object $component, array &$data): void
Expand Down Expand Up @@ -159,21 +174,6 @@ private function mount(object $component, array &$data): void
$component->mount(...$parameters);
}

private function getComponent(string $name): object
{
$name = $this->classMap[$name] ?? $name;

if (!$this->components->has($name)) {
if ($this->isAnonymousComponent($name)) {
return new AnonymousComponent();
}

$this->throwUnknownComponentException($name);
}

return $this->components->get($name);
}

private function preMount(object $component, array $data, ComponentMetadata $componentMetadata): array
{
$event = new PreMountEvent($component, $data, $componentMetadata);
Expand Down Expand Up @@ -215,11 +215,6 @@ private function postMount(object $component, array $data, ComponentMetadata $co
];
}

private function isAnonymousComponent(string $name): bool
{
return null !== $this->componentTemplateFinder->findAnonymousComponentTemplate($name);
}

/**
* @return never
*/
Expand Down
5 changes: 5 additions & 0 deletions src/TwigComponent/src/ComponentMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public function isPublicPropsExposed(): bool
return $this->get('expose_public_props', false);
}

public function isAnonymous(): bool
{
return !isset($this->config['service_id']);
}

public function getAttributesVar(): string
{
return $this->get('attributes_var', 'attributes');
Expand Down
92 changes: 92 additions & 0 deletions src/TwigComponent/tests/Unit/ComponentFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\TwigComponent\Tests\Unit;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;

/**
* @author Simon André <smn.andre@gmail.com>
*/
class ComponentFactoryTest extends TestCase
{
public function testMetadataForConfig(): void
{
$factory = new ComponentFactory(
$this->createMock(ComponentTemplateFinderInterface::class),
$this->createMock(ServiceLocator::class),
$this->createMock(PropertyAccessorInterface::class),
$this->createMock(EventDispatcherInterface::class),
['foo' => ['key' => 'foo', 'template' => 'bar.html.twig']],
[]
);

$metadata = $factory->metadataFor('foo');

$this->assertSame('foo', $metadata->getName());
$this->assertSame('bar.html.twig', $metadata->getTemplate());
}

public function testMetadataForResolveAlias(): void
{
$factory = new ComponentFactory(
$this->createMock(ComponentTemplateFinderInterface::class),
$this->createMock(ServiceLocator::class),
$this->createMock(PropertyAccessorInterface::class),
$this->createMock(EventDispatcherInterface::class),
[
'bar' => ['key' => 'bar', 'template' => 'bar.html.twig'],
'foo' => ['key' => 'foo', 'template' => 'foo.html.twig'],
],
['Foo\\Bar' => 'bar'],
);

$metadata = $factory->metadataFor('Foo\\Bar');

$this->assertSame('bar', $metadata->getName());
$this->assertSame('bar.html.twig', $metadata->getTemplate());
}

public function testMetadataForReuseAnonymousConfig(): void
{
$templateFinder = $this->createMock(ComponentTemplateFinderInterface::class);
$templateFinder->expects($this->atLeastOnce())
->method('findAnonymousComponentTemplate')
->with('foo')
->willReturnOnConsecutiveCalls('foo.html.twig', 'bar.html.twig', 'bar.html.twig');

$factory = new ComponentFactory(
$templateFinder,
$this->createMock(ServiceLocator::class),
$this->createMock(PropertyAccessorInterface::class),
$this->createMock(EventDispatcherInterface::class),
[],
[]
);

$metadata = $factory->metadataFor('foo');
$this->assertSame('foo', $metadata->getName());
$this->assertSame('foo.html.twig', $metadata->getTemplate());

$metadata = $factory->metadataFor('foo');
$this->assertSame('foo', $metadata->getName());
$this->assertSame('foo.html.twig', $metadata->getTemplate());

$metadata = $factory->metadataFor('foo');
$this->assertSame('foo', $metadata->getName());
$this->assertSame('foo.html.twig', $metadata->getTemplate());
}
}

0 comments on commit c7e4532

Please # to comment.