vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php line 74
<?php/** This file is part of the Symfony package.** (c) Fabien Potencier <fabien@symfony.com>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace Symfony\Bridge\Doctrine\Form\Type;use Doctrine\Common\Collections\Collection;use Doctrine\Persistence\ManagerRegistry;use Doctrine\Persistence\ObjectManager;use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader;use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader;use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;use Symfony\Component\Form\AbstractType;use Symfony\Component\Form\ChoiceList\ChoiceList;use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;use Symfony\Component\Form\Exception\RuntimeException;use Symfony\Component\Form\Extension\Core\Type\ChoiceType;use Symfony\Component\Form\FormBuilderInterface;use Symfony\Component\OptionsResolver\Options;use Symfony\Component\OptionsResolver\OptionsResolver;use Symfony\Contracts\Service\ResetInterface;abstract class DoctrineType extends AbstractType implements ResetInterface{/*** @var ManagerRegistry*/protected $registry;/*** @var IdReader[]*/private array $idReaders = [];/*** @var EntityLoaderInterface[]*/private array $entityLoaders = [];/*** Creates the label for a choice.** For backwards compatibility, objects are cast to strings by default.** @internal This method is public to be usable as callback. It should not* be used in user code.*/public static function createChoiceLabel(object $choice): string{return (string) $choice;}/*** Creates the field name for a choice.** This method is used to generate field names if the underlying object has* a single-column integer ID. In that case, the value of the field is* the ID of the object. That ID is also used as field name.** @param string $value The choice value. Corresponds to the object's ID here.** @internal This method is public to be usable as callback. It should not* be used in user code.*/public static function createChoiceName(object $choice, int|string $key, string $value): string{return str_replace('-', '_', $value);}/*** Gets important parts from QueryBuilder that will allow to cache its results.* For instance in ORM two query builders with an equal SQL string and* equal parameters are considered to be equal.** @param object $queryBuilder A query builder, type declaration is not present here as there* is no common base class for the different implementations** @internal This method is public to be usable as callback. It should not* be used in user code.*/public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array{return null;}public function __construct(ManagerRegistry $registry){$this->registry = $registry;}public function buildForm(FormBuilderInterface $builder, array $options){if ($options['multiple'] && interface_exists(Collection::class)) {$builder->addEventSubscriber(new MergeDoctrineCollectionListener())->addViewTransformer(new CollectionToArrayTransformer(), true);}}public function configureOptions(OptionsResolver $resolver){$choiceLoader = function (Options $options) {// Unless the choices are given explicitly, load them on demandif (null === $options['choices']) {// If there is no QueryBuilder we can safely cache$vary = [$options['em'], $options['class']];// also if concrete Type can return important QueryBuilder parts to generate// hash key we go for it as well, otherwise fallback on the instanceif ($options['query_builder']) {$vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];}return ChoiceList::loader($this, new DoctrineChoiceLoader($options['em'],$options['class'],$options['id_reader'],$this->getCachedEntityLoader($options['em'],$options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),$options['class'],$vary)), $vary);}return null;};$choiceName = function (Options $options) {// If the object has a single-column, numeric ID, use that ID as// field name. We can only use numeric IDs as names, as we cannot// guarantee that a non-numeric ID contains a valid form nameif ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']);}// Otherwise, an incrementing integer is used as name automaticallyreturn null;};// The choices are always indexed by ID (see "choices" normalizer// and DoctrineChoiceLoader), unless the ID is composite. Then they// are indexed by an incrementing integer.// Use the ID/incrementing integer as choice value.$choiceValue = function (Options $options) {// If the entity has a single-column ID, use that ID as valueif ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {return ChoiceList::value($this, $options['id_reader']->getIdValue(...), $options['id_reader']);}// Otherwise, an incrementing integer is used as value automaticallyreturn null;};$emNormalizer = function (Options $options, $em) {if (null !== $em) {if ($em instanceof ObjectManager) {return $em;}return $this->registry->getManager($em);}$em = $this->registry->getManagerForClass($options['class']);if (null === $em) {throw new RuntimeException(sprintf('Class "%s" seems not to be a managed Doctrine entity. Did you forget to map it?', $options['class']));}return $em;};// Invoke the query builder closure so that we can cache choice lists// for equal query builders$queryBuilderNormalizer = function (Options $options, $queryBuilder) {if (\is_callable($queryBuilder)) {$queryBuilder = $queryBuilder($options['em']->getRepository($options['class']));}return $queryBuilder;};// Set the "id_reader" option via the normalizer. This option is not// supposed to be set by the user.$idReaderNormalizer = function (Options $options) {// The ID reader is a utility that is needed to read the object IDs// when generating the field values. The callback generating the// field values has no access to the object manager or the class// of the field, so we store that information in the reader.// The reader is cached so that two choice lists for the same class// (and hence with the same reader) can successfully be cached.return $this->getCachedIdReader($options['em'], $options['class']);};$resolver->setDefaults(['em' => null,'query_builder' => null,'choices' => null,'choice_loader' => $choiceLoader,'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),'choice_name' => $choiceName,'choice_value' => $choiceValue,'id_reader' => null, // internal'choice_translation_domain' => false,]);$resolver->setRequired(['class']);$resolver->setNormalizer('em', $emNormalizer);$resolver->setNormalizer('query_builder', $queryBuilderNormalizer);$resolver->setNormalizer('id_reader', $idReaderNormalizer);$resolver->setAllowedTypes('em', ['null', 'string', ObjectManager::class]);}/*** Return the default loader object.*/abstract public function getLoader(ObjectManager $manager, object $queryBuilder, string $class): EntityLoaderInterface;public function getParent(): string{return ChoiceType::class;}public function reset(){$this->idReaders = [];$this->entityLoaders = [];}private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader{$hash = CachingFactoryDecorator::generateHash([$manager, $class]);if (isset($this->idReaders[$hash])) {return $this->idReaders[$hash];}$idReader = new IdReader($manager, $manager->getClassMetadata($class));// don't cache the instance for composite ids that cannot be optimizedreturn $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null;}private function getCachedEntityLoader(ObjectManager $manager, object $queryBuilder, string $class, array $vary): EntityLoaderInterface{$hash = CachingFactoryDecorator::generateHash($vary);return $this->entityLoaders[$hash] ??= $this->getLoader($manager, $queryBuilder, $class);}}