vendor/symfony/ux-twig-component/src/ComponentRenderer.php line 52

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 Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  12. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  13. use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
  14. use Symfony\UX\TwigComponent\Event\PostRenderEvent;
  15. use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
  16. use Symfony\UX\TwigComponent\Event\PreRenderEvent;
  17. use Twig\Environment;
  18. use Twig\Extension\EscaperExtension;
  19. /**
  20. * @author Kevin Bond <kevinbond@gmail.com>
  21. *
  22. * @internal
  23. */
  24. final class ComponentRenderer implements ComponentRendererInterface
  25. {
  26. private bool $safeClassesRegistered = false;
  27. public function __construct(
  28. private Environment $twig,
  29. private EventDispatcherInterface $dispatcher,
  30. private ComponentFactory $factory,
  31. private PropertyAccessorInterface $propertyAccessor,
  32. private ComponentStack $componentStack,
  33. ) {
  34. }
  35. /**
  36. * Allow the render process to be short-circuited.
  37. */
  38. public function preCreateForRender(string $name, array $props = []): ?string
  39. {
  40. $event = new PreCreateForRenderEvent($name, $props);
  41. $this->dispatcher->dispatch($event);
  42. return $event->getRenderedString();
  43. }
  44. public function createAndRender(string $name, array $props = []): string
  45. {
  46. if ($preRendered = $this->preCreateForRender($name, $props)) {
  47. return $preRendered;
  48. }
  49. return $this->render($this->factory->create($name, $props));
  50. }
  51. public function render(MountedComponent $mounted): string
  52. {
  53. $this->componentStack->push($mounted);
  54. $event = $this->preRender($mounted);
  55. $variables = $event->getVariables();
  56. // see ComponentNode. When rendering an individual embedded component,
  57. // *not* through its parent, we need to set the parent template.
  58. if ($event->getTemplateIndex()) {
  59. $variables['__parent__'] = $event->getParentTemplateForEmbedded();
  60. }
  61. try {
  62. if ($this->twig::MAJOR_VERSION < 3) {
  63. return $this->twig->loadTemplate($event->getTemplate(), $event->getTemplateIndex())->render($variables);
  64. }
  65. return $this->twig->loadTemplate(
  66. $this->twig->getTemplateClass($event->getTemplate()),
  67. $event->getTemplate(),
  68. $event->getTemplateIndex(),
  69. )->render($variables);
  70. } finally {
  71. $mounted = $this->componentStack->pop();
  72. $event = new PostRenderEvent($mounted);
  73. $this->dispatcher->dispatch($event);
  74. }
  75. }
  76. public function startEmbeddedComponentRender(string $name, array $props, array $context, string $hostTemplateName, int $index): PreRenderEvent
  77. {
  78. $context[PreRenderEvent::EMBEDDED] = true;
  79. $mounted = $this->factory->create($name, $props);
  80. $mounted->addExtraMetadata('hostTemplate', $hostTemplateName);
  81. $mounted->addExtraMetadata('embeddedTemplateIndex', $index);
  82. $this->componentStack->push($mounted);
  83. return $this->preRender($mounted, $context);
  84. }
  85. public function finishEmbeddedComponentRender(): void
  86. {
  87. $mounted = $this->componentStack->pop();
  88. $event = new PostRenderEvent($mounted);
  89. $this->dispatcher->dispatch($event);
  90. }
  91. private function preRender(MountedComponent $mounted, array $context = []): PreRenderEvent
  92. {
  93. if (!$this->safeClassesRegistered) {
  94. $this->twig->getExtension(EscaperExtension::class)->addSafeClass(ComponentAttributes::class, ['html']);
  95. $this->safeClassesRegistered = true;
  96. }
  97. $component = $mounted->getComponent();
  98. $metadata = $this->factory->metadataFor($mounted->getName());
  99. // expose public properties and properties marked with ExposeInTemplate attribute
  100. $props = iterator_to_array($this->exposedVariables($component, $metadata->isPublicPropsExposed()));
  101. $variables = array_merge(
  102. // first so values can be overridden
  103. $context,
  104. // keep reference to old context
  105. ['outerScope' => $context],
  106. // add the component as "this"
  107. ['this' => $component],
  108. // add computed properties proxy
  109. ['computed' => new ComputedPropertiesProxy($component)],
  110. // add attributes
  111. [$metadata->getAttributesVar() => $mounted->getAttributes()],
  112. $props,
  113. ['__props' => $props]
  114. );
  115. $event = new PreRenderEvent($mounted, $metadata, $variables);
  116. $this->dispatcher->dispatch($event);
  117. return $event;
  118. }
  119. private function exposedVariables(object $component, bool $exposePublicProps): \Iterator
  120. {
  121. if ($exposePublicProps) {
  122. yield from get_object_vars($component);
  123. }
  124. $class = new \ReflectionClass($component);
  125. foreach ($class->getProperties() as $property) {
  126. if (!$attribute = $property->getAttributes(ExposeInTemplate::class)[0] ?? null) {
  127. continue;
  128. }
  129. $attribute = $attribute->newInstance();
  130. /** @var ExposeInTemplate $attribute */
  131. $value = $attribute->getter ? $component->{rtrim($attribute->getter, '()')}() : $this->propertyAccessor->getValue($component, $property->name);
  132. if ($attribute->destruct) {
  133. foreach ($value as $key => $destructedValue) {
  134. yield $key => $destructedValue;
  135. }
  136. }
  137. yield $attribute->name ?? $property->name => $value;
  138. }
  139. foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
  140. if (!$attribute = $method->getAttributes(ExposeInTemplate::class)[0] ?? null) {
  141. continue;
  142. }
  143. $attribute = $attribute->newInstance();
  144. /** @var ExposeInTemplate $attribute */
  145. $name = $attribute->name ?? (str_starts_with($method->name, 'get') ? lcfirst(substr($method->name, 3)) : $method->name);
  146. if ($method->getNumberOfRequiredParameters()) {
  147. throw new \LogicException(sprintf('Cannot use %s on methods with required parameters (%s::%s).', ExposeInTemplate::class, $component::class, $method->name));
  148. }
  149. if ($attribute->destruct) {
  150. foreach ($component->{$method->name}() as $prop => $value) {
  151. yield $prop => $value;
  152. }
  153. return;
  154. }
  155. yield $name => $component->{$method->name}();
  156. }
  157. }
  158. }