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

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