vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php line 118

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <[email protected]>
  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\Bridge\Doctrine\Form\Type;
  11. use Doctrine\Common\Collections\Collection;
  12. use Doctrine\Persistence\ManagerRegistry;
  13. use Doctrine\Persistence\ObjectManager;
  14. use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader;
  15. use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;
  16. use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader;
  17. use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
  18. use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
  19. use Symfony\Component\Form\AbstractType;
  20. use Symfony\Component\Form\ChoiceList\ChoiceList;
  21. use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
  22. use Symfony\Component\Form\Exception\RuntimeException;
  23. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  24. use Symfony\Component\Form\FormBuilderInterface;
  25. use Symfony\Component\OptionsResolver\Options;
  26. use Symfony\Component\OptionsResolver\OptionsResolver;
  27. use Symfony\Contracts\Service\ResetInterface;
  28. abstract class DoctrineType extends AbstractType implements ResetInterface
  29. {
  30. /**
  31. * @var ManagerRegistry
  32. */
  33. protected $registry;
  34. /**
  35. * @var IdReader[]
  36. */
  37. private $idReaders = [];
  38. /**
  39. * @var EntityLoaderInterface[]
  40. */
  41. private $entityLoaders = [];
  42. /**
  43. * Creates the label for a choice.
  44. *
  45. * For backwards compatibility, objects are cast to strings by default.
  46. *
  47. * @internal This method is public to be usable as callback. It should not
  48. * be used in user code.
  49. */
  50. public static function createChoiceLabel(object $choice): string
  51. {
  52. return (string) $choice;
  53. }
  54. /**
  55. * Creates the field name for a choice.
  56. *
  57. * This method is used to generate field names if the underlying object has
  58. * a single-column integer ID. In that case, the value of the field is
  59. * the ID of the object. That ID is also used as field name.
  60. *
  61. * @param int|string $key The choice key
  62. * @param string $value The choice value. Corresponds to the object's
  63. * ID here.
  64. *
  65. * @internal This method is public to be usable as callback. It should not
  66. * be used in user code.
  67. */
  68. public static function createChoiceName(object $choice, $key, string $value): string
  69. {
  70. return str_replace('-', '_', $value);
  71. }
  72. /**
  73. * Gets important parts from QueryBuilder that will allow to cache its results.
  74. * For instance in ORM two query builders with an equal SQL string and
  75. * equal parameters are considered to be equal.
  76. *
  77. * @param object $queryBuilder A query builder, type declaration is not present here as there
  78. * is no common base class for the different implementations
  79. *
  80. * @internal This method is public to be usable as callback. It should not
  81. * be used in user code.
  82. */
  83. public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array
  84. {
  85. return null;
  86. }
  87. public function __construct(ManagerRegistry $registry)
  88. {
  89. $this->registry = $registry;
  90. }
  91. public function buildForm(FormBuilderInterface $builder, array $options)
  92. {
  93. if ($options['multiple'] && interface_exists(Collection::class)) {
  94. $builder
  95. ->addEventSubscriber(new MergeDoctrineCollectionListener())
  96. ->addViewTransformer(new CollectionToArrayTransformer(), true)
  97. ;
  98. }
  99. }
  100. public function configureOptions(OptionsResolver $resolver)
  101. {
  102. $choiceLoader = function (Options $options) {
  103. // Unless the choices are given explicitly, load them on demand
  104. if (null === $options['choices']) {
  105. // If there is no QueryBuilder we can safely cache
  106. $vary = [$options['em'], $options['class']];
  107. // also if concrete Type can return important QueryBuilder parts to generate
  108. // hash key we go for it as well, otherwise fallback on the instance
  109. if ($options['query_builder']) {
  110. $vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];
  111. }
  112. return ChoiceList::loader($this, new DoctrineChoiceLoader(
  113. $options['em'],
  114. $options['class'],
  115. $options['id_reader'],
  116. $this->getCachedEntityLoader(
  117. $options['em'],
  118. $options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),
  119. $options['class'],
  120. $vary
  121. )
  122. ), $vary);
  123. }
  124. return null;
  125. };
  126. $choiceName = function (Options $options) {
  127. // If the object has a single-column, numeric ID, use that ID as
  128. // field name. We can only use numeric IDs as names, as we cannot
  129. // guarantee that a non-numeric ID contains a valid form name
  130. if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {
  131. return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']);
  132. }
  133. // Otherwise, an incrementing integer is used as name automatically
  134. return null;
  135. };
  136. // The choices are always indexed by ID (see "choices" normalizer
  137. // and DoctrineChoiceLoader), unless the ID is composite. Then they
  138. // are indexed by an incrementing integer.
  139. // Use the ID/incrementing integer as choice value.
  140. $choiceValue = function (Options $options) {
  141. // If the entity has a single-column ID, use that ID as value
  142. if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {
  143. return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']);
  144. }
  145. // Otherwise, an incrementing integer is used as value automatically
  146. return null;
  147. };
  148. $emNormalizer = function (Options $options, $em) {
  149. if (null !== $em) {
  150. if ($em instanceof ObjectManager) {
  151. return $em;
  152. }
  153. return $this->registry->getManager($em);
  154. }
  155. $em = $this->registry->getManagerForClass($options['class']);
  156. if (null === $em) {
  157. throw new RuntimeException(sprintf('Class "%s" seems not to be a managed Doctrine entity. Did you forget to map it?', $options['class']));
  158. }
  159. return $em;
  160. };
  161. // Invoke the query builder closure so that we can cache choice lists
  162. // for equal query builders
  163. $queryBuilderNormalizer = function (Options $options, $queryBuilder) {
  164. if (\is_callable($queryBuilder)) {
  165. $queryBuilder = $queryBuilder($options['em']->getRepository($options['class']));
  166. }
  167. return $queryBuilder;
  168. };
  169. // Set the "id_reader" option via the normalizer. This option is not
  170. // supposed to be set by the user.
  171. $idReaderNormalizer = function (Options $options) {
  172. // The ID reader is a utility that is needed to read the object IDs
  173. // when generating the field values. The callback generating the
  174. // field values has no access to the object manager or the class
  175. // of the field, so we store that information in the reader.
  176. // The reader is cached so that two choice lists for the same class
  177. // (and hence with the same reader) can successfully be cached.
  178. return $this->getCachedIdReader($options['em'], $options['class']);
  179. };
  180. $resolver->setDefaults([
  181. 'em' => null,
  182. 'query_builder' => null,
  183. 'choices' => null,
  184. 'choice_loader' => $choiceLoader,
  185. 'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
  186. 'choice_name' => $choiceName,
  187. 'choice_value' => $choiceValue,
  188. 'id_reader' => null, // internal
  189. 'choice_translation_domain' => false,
  190. ]);
  191. $resolver->setRequired(['class']);
  192. $resolver->setNormalizer('em', $emNormalizer);
  193. $resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
  194. $resolver->setNormalizer('id_reader', $idReaderNormalizer);
  195. $resolver->setAllowedTypes('em', ['null', 'string', ObjectManager::class]);
  196. }
  197. /**
  198. * Return the default loader object.
  199. *
  200. * @return EntityLoaderInterface
  201. */
  202. abstract public function getLoader(ObjectManager $manager, object $queryBuilder, string $class);
  203. /**
  204. * @return string
  205. */
  206. public function getParent()
  207. {
  208. return ChoiceType::class;
  209. }
  210. public function reset()
  211. {
  212. $this->idReaders = [];
  213. $this->entityLoaders = [];
  214. }
  215. private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader
  216. {
  217. $hash = CachingFactoryDecorator::generateHash([$manager, $class]);
  218. if (isset($this->idReaders[$hash])) {
  219. return $this->idReaders[$hash];
  220. }
  221. $idReader = new IdReader($manager, $manager->getClassMetadata($class));
  222. // don't cache the instance for composite ids that cannot be optimized
  223. return $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null;
  224. }
  225. private function getCachedEntityLoader(ObjectManager $manager, object $queryBuilder, string $class, array $vary): EntityLoaderInterface
  226. {
  227. $hash = CachingFactoryDecorator::generateHash($vary);
  228. return $this->entityLoaders[$hash] ?? ($this->entityLoaders[$hash] = $this->getLoader($manager, $queryBuilder, $class));
  229. }
  230. }