vendor/symfony/ux-twig-component/src/ComponentFactory.php line 42

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\UX\TwigComponent;
  11. use Psr\EventDispatcher\EventDispatcherInterface;
  12. use Symfony\Component\DependencyInjection\ServiceLocator;
  13. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  14. use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
  15. use Symfony\UX\TwigComponent\Event\PostMountEvent;
  16. use Symfony\UX\TwigComponent\Event\PreMountEvent;
  17. /**
  18.  * @author Kevin Bond <kevinbond@gmail.com>
  19.  *
  20.  * @internal
  21.  */
  22. final class ComponentFactory
  23. {
  24.     /**
  25.      * @param array<string, array> $config
  26.      */
  27.     public function __construct(
  28.         private ServiceLocator $components,
  29.         private PropertyAccessorInterface $propertyAccessor,
  30.         private EventDispatcherInterface $eventDispatcher,
  31.         private array $config
  32.     ) {
  33.     }
  34.     public function metadataFor(string $name): ComponentMetadata
  35.     {
  36.         if (!$config $this->config[$name] ?? null) {
  37.             throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s'$nameimplode(', 'array_keys($this->config))));
  38.         }
  39.         return new ComponentMetadata($config);
  40.     }
  41.     /**
  42.      * Creates the component and "mounts" it with the passed data.
  43.      */
  44.     public function create(string $name, array $data = []): MountedComponent
  45.     {
  46.         return $this->mountFromObject(
  47.             $this->getComponent($name),
  48.             $data,
  49.             $this->metadataFor($name)
  50.         );
  51.     }
  52.     /**
  53.      * @internal
  54.      */
  55.     public function mountFromObject(object $component, array $dataComponentMetadata $componentMetadata): MountedComponent
  56.     {
  57.         $originalData $data;
  58.         $data $this->preMount($component$data);
  59.         $this->mount($component$data);
  60.         // set data that wasn't set in mount on the component directly
  61.         foreach ($data as $property => $value) {
  62.             if ($this->propertyAccessor->isWritable($component$property)) {
  63.                 $this->propertyAccessor->setValue($component$property$value);
  64.                 unset($data[$property]);
  65.             }
  66.         }
  67.         $data $this->postMount($component$data);
  68.         // create attributes from "attributes" key if exists
  69.         $attributesVar $componentMetadata->getAttributesVar();
  70.         $attributes $data[$attributesVar] ?? [];
  71.         unset($data[$attributesVar]);
  72.         // ensure remaining data is scalar
  73.         foreach ($data as $key => $value) {
  74.             if ($value instanceof \Stringable) {
  75.                 $data[$key] = (string) $value;
  76.                 continue;
  77.             }
  78.             if (!\is_scalar($value) && null !== $value) {
  79.                 throw new \LogicException(sprintf('A "%s" prop was passed when creating the "%s" component. No matching %s property or mount() argument was found, so we attempted to use this as an HTML attribute. But, the value is not a scalar (it\'s a %s). Did you mean to pass this to your component or is there a typo on its name?'$key$componentMetadata->getName(), $keyget_debug_type($value)));
  80.             }
  81.         }
  82.         return new MountedComponent(
  83.             $componentMetadata->getName(),
  84.             $component,
  85.             new ComponentAttributes(array_merge($attributes$data)),
  86.             $originalData
  87.         );
  88.     }
  89.     /**
  90.      * Returns the "unmounted" component.
  91.      */
  92.     public function get(string $name): object
  93.     {
  94.         return $this->getComponent($name);
  95.     }
  96.     private function mount(object $component, array &$data): void
  97.     {
  98.         try {
  99.             $method = (new \ReflectionClass($component))->getMethod('mount');
  100.         } catch (\ReflectionException) {
  101.             // no hydrate method
  102.             return;
  103.         }
  104.         $parameters = [];
  105.         foreach ($method->getParameters() as $refParameter) {
  106.             $name $refParameter->getName();
  107.             if (\array_key_exists($name$data)) {
  108.                 $parameters[] = $data[$name];
  109.                 // remove the data element so it isn't used to set the property directly.
  110.                 unset($data[$name]);
  111.             } elseif ($refParameter->isDefaultValueAvailable()) {
  112.                 $parameters[] = $refParameter->getDefaultValue();
  113.             } else {
  114.                 throw new \LogicException(sprintf('%s::mount() has a required $%s parameter. Make sure this is passed or make give a default value.'\get_class($component), $refParameter->getName()));
  115.             }
  116.         }
  117.         $component->mount(...$parameters);
  118.     }
  119.     private function getComponent(string $name): object
  120.     {
  121.         if (!$this->components->has($name)) {
  122.             throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s'$nameimplode(', 'array_keys($this->components->getProvidedServices()))));
  123.         }
  124.         return $this->components->get($name);
  125.     }
  126.     private function preMount(object $component, array $data): array
  127.     {
  128.         $event = new PreMountEvent($component$data);
  129.         $this->eventDispatcher->dispatch($event);
  130.         $data $event->getData();
  131.         foreach (AsTwigComponent::preMountMethods($component) as $method) {
  132.             $newData $component->{$method->name}($data);
  133.             if (null !== $newData) {
  134.                 $data $newData;
  135.             }
  136.         }
  137.         return $data;
  138.     }
  139.     private function postMount(object $component, array $data): array
  140.     {
  141.         $event = new PostMountEvent($component$data);
  142.         $this->eventDispatcher->dispatch($event);
  143.         $data $event->getData();
  144.         foreach (AsTwigComponent::postMountMethods($component) as $method) {
  145.             $newData $component->{$method->name}($data);
  146.             if (null !== $newData) {
  147.                 $data $newData;
  148.             }
  149.         }
  150.         return $data;
  151.     }
  152. }