vendor/friendsofsymfony/elastica-bundle/src/Transformer/ModelToElasticaAutoTransformer.php line 61

  1. <?php
  2. /*
  3. * This file is part of the FOSElasticaBundle package.
  4. *
  5. * (c) FriendsOfSymfony <https://friendsofsymfony.github.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 FOS\ElasticaBundle\Transformer;
  11. use Elastica\Document;
  12. use FOS\ElasticaBundle\Event\PostTransformEvent;
  13. use FOS\ElasticaBundle\Event\PreTransformEvent;
  14. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  15. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  16. /**
  17. * Maps Elastica documents with Doctrine objects
  18. * This mapper assumes an exact match between
  19. * elastica documents ids and doctrine object ids.
  20. *
  21. * @phpstan-import-type TFields from ModelToElasticaTransformerInterface
  22. *
  23. * @phpstan-type TOptions = array{identifier: string, index: string}
  24. */
  25. class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterface
  26. {
  27. /**
  28. * @var ?EventDispatcherInterface
  29. */
  30. protected $dispatcher;
  31. /**
  32. * Optional parameters.
  33. *
  34. * @var array
  35. *
  36. * @phpstan-var TOptions
  37. */
  38. protected $options = [
  39. 'identifier' => 'id',
  40. 'index' => '',
  41. ];
  42. /**
  43. * PropertyAccessor instance.
  44. *
  45. * @var PropertyAccessorInterface
  46. */
  47. protected $propertyAccessor;
  48. /**
  49. * Instanciates a new Mapper.
  50. *
  51. * @phpstan-param array<string, mixed> $options
  52. */
  53. public function __construct(array $options = [], ?EventDispatcherInterface $dispatcher = null)
  54. {
  55. $this->options = \array_merge($this->options, $options);
  56. $this->dispatcher = $dispatcher;
  57. }
  58. /**
  59. * Set the PropertyAccessor.
  60. */
  61. public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor): void
  62. {
  63. $this->propertyAccessor = $propertyAccessor;
  64. }
  65. /**
  66. * Transforms an object into an elastica object having the required keys.
  67. */
  68. public function transform(object $object, array $fields): Document
  69. {
  70. $identifier = $this->propertyAccessor->getValue($object, $this->options['identifier']);
  71. return $this->transformObjectToDocument($object, $fields, (string) $identifier);
  72. }
  73. /**
  74. * transform a nested document or an object property into an array of ElasticaDocument.
  75. *
  76. * @param array<object>|\Traversable<object>|\ArrayAccess<mixed,mixed>|null $objects the object to convert
  77. * @param array $fields the keys we want to have in the returned array
  78. *
  79. * @phpstan-param TFields $fields
  80. *
  81. * @return array<mixed>
  82. */
  83. protected function transformNested($objects, array $fields): ?array
  84. {
  85. if (\is_iterable($objects)) {
  86. $documents = [];
  87. foreach ($objects as $object) {
  88. $document = $this->transformObjectToDocument($object, $fields);
  89. $documents[] = $document->getData();
  90. }
  91. return $documents;
  92. }
  93. if (null !== $objects) {
  94. $document = $this->transformObjectToDocument($objects, $fields);
  95. return $document->getData();
  96. }
  97. return null;
  98. }
  99. /**
  100. * Attempts to convert any type to a string or an array of strings.
  101. *
  102. * @param mixed $value
  103. *
  104. * @return string|list<string>
  105. */
  106. protected function normalizeValue($value)
  107. {
  108. $normalizeValue = static function (&$v) {
  109. if ($v instanceof \DateTimeInterface) {
  110. $v = $v->format('c');
  111. } elseif ($v instanceof \DateInterval) {
  112. $v = $v->format('P%yY%mM%dDT%hH%iM%sS');
  113. } elseif (\PHP_VERSION_ID >= 80100 && $v instanceof \BackedEnum) {
  114. $v = $v->value;
  115. } elseif (!\is_scalar($v) && null !== $v) {
  116. $v = (string) $v;
  117. }
  118. };
  119. if (\is_iterable($value)) {
  120. $value = \is_array($value) ? $value : \iterator_to_array($value, false);
  121. \array_walk_recursive($value, $normalizeValue);
  122. } else {
  123. $normalizeValue($value);
  124. }
  125. return $value;
  126. }
  127. /**
  128. * Transforms the given object to an elastica document.
  129. *
  130. * @phpstan-param TFields $fields
  131. */
  132. protected function transformObjectToDocument(object $object, array $fields, string $identifier = ''): Document
  133. {
  134. $document = new Document($identifier, [], $this->options['index']);
  135. if ($this->dispatcher) {
  136. $this->dispatcher->dispatch($event = new PreTransformEvent($document, $fields, $object));
  137. $document = $event->getDocument();
  138. }
  139. foreach ($fields as $key => $mapping) {
  140. $path = $mapping['property_path'] ?? $key;
  141. if (false === $path) {
  142. continue;
  143. }
  144. $value = $this->propertyAccessor->getValue($object, $path);
  145. if (isset($mapping['properties'], $mapping['type'])
  146. && $mapping['properties']
  147. && \in_array($mapping['type'], ['nested', 'object'], true)
  148. ) {
  149. /* $value is a nested document or object. Transform $value into
  150. * an array of documents, respective the mapped properties.
  151. */
  152. $document->set($key, $this->transformNested($value, $mapping['properties']));
  153. continue;
  154. }
  155. if ('attachment' === ($mapping['type'] ?? null)) {
  156. // $value is an attachment. Add it to the document.
  157. if ($value instanceof \SplFileInfo) {
  158. $document->addFile($key, $value->getPathName());
  159. } else {
  160. $document->addFileContent($key, $value);
  161. }
  162. continue;
  163. }
  164. $document->set($key, $this->normalizeValue($value));
  165. }
  166. if ($this->dispatcher) {
  167. $this->dispatcher->dispatch($event = new PostTransformEvent($document, $fields, $object));
  168. $document = $event->getDocument();
  169. }
  170. return $document;
  171. }
  172. }