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

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\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$keystring $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 $managerobject $queryBuilderstring $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 $managerstring $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 $managerobject $queryBuilderstring $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. }