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

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