This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
<?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\ChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
/**
* Loads choices using a Doctrine object manager.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DoctrineChoiceLoader implements ChoiceLoaderInterface
{
private $factory;
private $manager;
private $class;
private $idReader;
private $objectLoader;
/**
* @var ChoiceListInterface
*/
private $choiceList;
/**
* Creates a new choice loader.
*
* Optionally, an implementation of {@link EntityLoaderInterface} can be
* passed which optimizes the object loading for one of the Doctrine
* mapper implementations.
*
* @param ChoiceListFactoryInterface $factory The factory for creating the loaded choice list
* @param ObjectManager $manager The object manager
* @param string $class The class name of the loaded objects
* @param IdReader $idReader The reader for the object IDs
* @param EntityLoaderInterface|null $objectLoader The objects loader
*/
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader = null, EntityLoaderInterface $objectLoader = null)
{
$classMetadata = $manager->getClassMetadata($class);
$this->factory = $factory;
$this->manager = $manager;
$this->class = $classMetadata->getName();
$this->idReader = $idReader ?: new IdReader($manager, $classMetadata);
$this->objectLoader = $objectLoader;
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if ($this->choiceList) {
return $this->choiceList;
}
$objects = $this->objectLoader
? $this->objectLoader->getEntities()
: $this->manager->getRepository($this->class)->findAll();
$this->choiceList = $this->factory->createListFromChoices($objects, $value);
return $this->choiceList;
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Performance optimization
if (empty($choices)) {
return array();
}
// Optimize performance for single-field identifiers. We already
// know that the IDs are used as values
$optimize = null === $value || \is_array($value) && $value[0] === $this->idReader;
// Attention: This optimization does not check choices for existence
if ($optimize && !$this->choiceList && $this->idReader->isSingleId()) {
$values = array();
// Maintain order and indices of the given objects
foreach ($choices as $i => $object) {
if ($object instanceof $this->class) {
// Make sure to convert to the right format
$values[$i] = (string) $this->idReader->getIdValue($object);
}
}
return $values;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Performance optimization
// Also prevents the generation of "WHERE id IN ()" queries through the
// object loader. At least with MySQL and on the development machine
// this was tested on, no exception was thrown for such invalid
// statements, consequently no test fails when this code is removed.
// https://github.com/symfony/symfony/pull/8981#issuecomment-24230557
if (empty($values)) {
return array();
}
// Optimize performance in case we have an object loader and
// a single-field identifier
$optimize = null === $value || \is_array($value) && $value[0] === $this->idReader;
if ($optimize && !$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) {
$unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values);
$objectsById = array();
$objects = array();
// Maintain order and indices from the given $values
// An alternative approach to the following loop is to add the
// "INDEX BY" clause to the Doctrine query in the loader,
// but I'm not sure whether that's doable in a generic fashion.
foreach ($unorderedObjects as $object) {
$objectsById[(string) $this->idReader->getIdValue($object)] = $object;
}
foreach ($values as $i => $id) {
if (isset($objectsById[$id])) {
$objects[$i] = $objectsById[$id];
}
}
return $objects;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
}

View File

@@ -0,0 +1,540 @@
<?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\ChoiceList;
@trigger_error('The '.__NAMESPACE__.'\EntityChoiceList class is deprecated since Symfony 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED);
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* A choice list presenting a list of Doctrine entities as choices.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link DoctrineChoiceLoader} instead.
*/
class EntityChoiceList extends ObjectChoiceList
{
/**
* @var ObjectManager
*/
private $em;
/**
* @var string
*/
private $class;
/**
* @var ClassMetadata
*/
private $classMetadata;
/**
* Metadata for target class of primary key association.
*
* @var ClassMetadata
*/
private $idClassMetadata;
/**
* Contains the query builder that builds the query for fetching the
* entities.
*
* This property should only be accessed through queryBuilder.
*
* @var EntityLoaderInterface
*/
private $entityLoader;
/**
* The identifier field, if the identifier is not composite.
*
* @var array
*/
private $idField = null;
/**
* Whether to use the identifier for index generation.
*
* @var bool
*/
private $idAsIndex = false;
/**
* Whether to use the identifier for value generation.
*
* @var bool
*/
private $idAsValue = false;
/**
* Whether the entities have already been loaded.
*
* @var bool
*/
private $loaded = false;
/**
* The preferred entities.
*
* @var array
*/
private $preferredEntities = array();
/**
* Creates a new entity choice list.
*
* @param ObjectManager $manager An EntityManager instance
* @param string $class The class name
* @param string $labelPath The property path used for the label
* @param EntityLoaderInterface $entityLoader An optional query builder
* @param array|\Traversable|null $entities An array of choices or null to lazy load
* @param array $preferredEntities An array of preferred choices
* @param string $groupPath A property path pointing to the property used
* to group the choices. Only allowed if
* the choices are given as flat array.
* @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths
*/
public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null)
{
$this->em = $manager;
$this->entityLoader = $entityLoader;
$this->classMetadata = $manager->getClassMetadata($class);
$this->class = $this->classMetadata->getName();
$this->loaded = \is_array($entities) || $entities instanceof \Traversable;
$this->preferredEntities = $preferredEntities;
list(
$this->idAsIndex,
$this->idAsValue,
$this->idField
) = $this->getIdentifierInfoForClass($this->classMetadata);
if (null !== $this->idField && $this->classMetadata->hasAssociation($this->idField)) {
$this->idClassMetadata = $this->em->getClassMetadata(
$this->classMetadata->getAssociationTargetClass($this->idField)
);
list(
$this->idAsIndex,
$this->idAsValue
) = $this->getIdentifierInfoForClass($this->idClassMetadata);
}
if (!$this->loaded) {
// Make sure the constraints of the parent constructor are
// fulfilled
$entities = array();
}
parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor);
}
/**
* Returns the list of entities.
*
* @return array
*
* @see ChoiceListInterface
*/
public function getChoices()
{
if (!$this->loaded) {
$this->load();
}
return parent::getChoices();
}
/**
* Returns the values for the entities.
*
* @return array
*
* @see ChoiceListInterface
*/
public function getValues()
{
if (!$this->loaded) {
$this->load();
}
return parent::getValues();
}
/**
* Returns the choice views of the preferred choices as nested array with
* the choice groups as top-level keys.
*
* @return array
*
* @see ChoiceListInterface
*/
public function getPreferredViews()
{
if (!$this->loaded) {
$this->load();
}
return parent::getPreferredViews();
}
/**
* Returns the choice views of the choices that are not preferred as nested
* array with the choice groups as top-level keys.
*
* @return array
*
* @see ChoiceListInterface
*/
public function getRemainingViews()
{
if (!$this->loaded) {
$this->load();
}
return parent::getRemainingViews();
}
/**
* Returns the entities corresponding to the given values.
*
* @return array
*
* @see ChoiceListInterface
*/
public function getChoicesForValues(array $values)
{
// Performance optimization
// Also prevents the generation of "WHERE id IN ()" queries through the
// entity loader. At least with MySQL and on the development machine
// this was tested on, no exception was thrown for such invalid
// statements, consequently no test fails when this code is removed.
// https://github.com/symfony/symfony/pull/8981#issuecomment-24230557
if (empty($values)) {
return array();
}
if (!$this->loaded) {
// Optimize performance in case we have an entity loader and
// a single-field identifier
if ($this->idAsValue && $this->entityLoader) {
$unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values);
$entitiesByValue = array();
$entities = array();
// Maintain order and indices from the given $values
// An alternative approach to the following loop is to add the
// "INDEX BY" clause to the Doctrine query in the loader,
// but I'm not sure whether that's doable in a generic fashion.
foreach ($unorderedEntities as $entity) {
$value = $this->fixValue($this->getSingleIdentifierValue($entity));
$entitiesByValue[$value] = $entity;
}
foreach ($values as $i => $value) {
if (isset($entitiesByValue[$value])) {
$entities[$i] = $entitiesByValue[$value];
}
}
return $entities;
}
$this->load();
}
return parent::getChoicesForValues($values);
}
/**
* Returns the values corresponding to the given entities.
*
* @return array
*
* @see ChoiceListInterface
*/
public function getValuesForChoices(array $entities)
{
// Performance optimization
if (empty($entities)) {
return array();
}
if (!$this->loaded) {
// Optimize performance for single-field identifiers. We already
// know that the IDs are used as values
// Attention: This optimization does not check choices for existence
if ($this->idAsValue) {
$values = array();
foreach ($entities as $i => $entity) {
if ($entity instanceof $this->class) {
// Make sure to convert to the right format
$values[$i] = $this->fixValue($this->getSingleIdentifierValue($entity));
}
}
return $values;
}
$this->load();
}
return parent::getValuesForChoices($entities);
}
/**
* Returns the indices corresponding to the given entities.
*
* @param array $entities
*
* @return array
*
* @see ChoiceListInterface
* @deprecated since version 2.4, to be removed in 3.0.
*/
public function getIndicesForChoices(array $entities)
{
@trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.4 and will be removed in 3.0.', E_USER_DEPRECATED);
// Performance optimization
if (empty($entities)) {
return array();
}
if (!$this->loaded) {
// Optimize performance for single-field identifiers. We already
// know that the IDs are used as indices
// Attention: This optimization does not check choices for existence
if ($this->idAsIndex) {
$indices = array();
foreach ($entities as $i => $entity) {
if ($entity instanceof $this->class) {
// Make sure to convert to the right format
$indices[$i] = $this->fixIndex($this->getSingleIdentifierValue($entity));
}
}
return $indices;
}
$this->load();
}
return parent::getIndicesForChoices($entities);
}
/**
* Returns the entities corresponding to the given values.
*
* @param array $values
*
* @return array
*
* @see ChoiceListInterface
* @deprecated since version 2.4, to be removed in 3.0.
*/
public function getIndicesForValues(array $values)
{
@trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.4 and will be removed in 3.0.', E_USER_DEPRECATED);
// Performance optimization
if (empty($values)) {
return array();
}
if (!$this->loaded) {
// Optimize performance for single-field identifiers.
// Attention: This optimization does not check values for existence
if ($this->idAsIndex && $this->idAsValue) {
return $this->fixIndices($values);
}
$this->load();
}
return parent::getIndicesForValues($values);
}
/**
* Creates a new unique index for this entity.
*
* If the entity has a single-field identifier, this identifier is used.
*
* Otherwise a new integer is generated.
*
* @param mixed $entity The choice to create an index for
*
* @return int|string a unique index containing only ASCII letters,
* digits and underscores
*/
protected function createIndex($entity)
{
if ($this->idAsIndex) {
return $this->fixIndex($this->getSingleIdentifierValue($entity));
}
return parent::createIndex($entity);
}
/**
* Creates a new unique value for this entity.
*
* If the entity has a single-field identifier, this identifier is used.
*
* Otherwise a new integer is generated.
*
* @param mixed $entity The choice to create a value for
*
* @return int|string A unique value without character limitations
*/
protected function createValue($entity)
{
if ($this->idAsValue) {
return (string) $this->getSingleIdentifierValue($entity);
}
return parent::createValue($entity);
}
/**
* {@inheritdoc}
*/
protected function fixIndex($index)
{
$index = parent::fixIndex($index);
// If the ID is a single-field integer identifier, it is used as
// index. Replace any leading minus by underscore to make it a valid
// form name.
if ($this->idAsIndex && $index < 0) {
$index = strtr($index, '-', '_');
}
return $index;
}
/**
* Get identifier information for a class.
*
* @param ClassMetadata $classMetadata The entity metadata
*
* @return array Return an array with idAsIndex, idAsValue and identifier
*/
private function getIdentifierInfoForClass(ClassMetadata $classMetadata)
{
$identifier = null;
$idAsIndex = false;
$idAsValue = false;
$identifiers = $classMetadata->getIdentifierFieldNames();
if (1 === \count($identifiers)) {
$identifier = $identifiers[0];
if (!$classMetadata->hasAssociation($identifier)) {
$idAsValue = true;
if (\in_array($classMetadata->getTypeOfField($identifier), array('integer', 'smallint', 'bigint'))) {
$idAsIndex = true;
}
}
}
return array($idAsIndex, $idAsValue, $identifier);
}
/**
* Loads the list with entities.
*
* @throws StringCastException
*/
private function load()
{
if ($this->entityLoader) {
$entities = $this->entityLoader->getEntities();
} else {
$entities = $this->em->getRepository($this->class)->findAll();
}
try {
// The second parameter $labels is ignored by ObjectChoiceList
parent::initialize($entities, array(), $this->preferredEntities);
} catch (StringCastException $e) {
throw new StringCastException(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), null, $e);
}
$this->loaded = true;
}
/**
* Returns the first (and only) value of the identifier fields of an entity.
*
* Doctrine must know about this entity, that is, the entity must already
* be persisted or added to the identity map before. Otherwise an
* exception is thrown.
*
* @param object $entity The entity for which to get the identifier
*
* @return array The identifier values
*
* @throws RuntimeException If the entity does not exist in Doctrine's identity map
*/
private function getSingleIdentifierValue($entity)
{
$value = current($this->getIdentifierValues($entity));
if ($this->idClassMetadata) {
$class = $this->idClassMetadata->getName();
if ($value instanceof $class) {
$value = current($this->idClassMetadata->getIdentifierValues($value));
}
}
return $value;
}
/**
* Returns the values of the identifier fields of an entity.
*
* Doctrine must know about this entity, that is, the entity must already
* be persisted or added to the identity map before. Otherwise an
* exception is thrown.
*
* @param object $entity The entity for which to get the identifier
*
* @return array The identifier values
*
* @throws RuntimeException If the entity does not exist in Doctrine's identity map
*/
private function getIdentifierValues($entity)
{
if (!$this->em->contains($entity)) {
throw new RuntimeException('Entities passed to the choice field must be managed. Maybe persist them in the entity manager?');
}
$this->em->initializeObject($entity);
return $this->classMetadata->getIdentifierValues($entity);
}
}

View File

@@ -0,0 +1,39 @@
<?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\ChoiceList;
/**
* Custom loader for entities in the choice list.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
interface EntityLoaderInterface
{
/**
* Returns an array of entities that are valid choices in the corresponding choice list.
*
* @return array The entities
*/
public function getEntities();
/**
* Returns an array of entities matching the given identifiers.
*
* @param string $identifier The identifier field of the object. This method
* is not applicable for fields with multiple
* identifiers.
* @param array $values The values of the identifiers
*
* @return array The entities
*/
public function getEntitiesByIds($identifier, array $values);
}

View File

@@ -0,0 +1,123 @@
<?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\ChoiceList;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\RuntimeException;
/**
* A utility for reading object IDs.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @internal this class is meant for internal use only
*/
class IdReader
{
private $om;
private $classMetadata;
private $singleId;
private $intId;
private $idField;
/**
* @var IdReader|null
*/
private $associationIdReader;
public function __construct(ObjectManager $om, ClassMetadata $classMetadata)
{
$ids = $classMetadata->getIdentifierFieldNames();
$idType = $classMetadata->getTypeOfField(current($ids));
$this->om = $om;
$this->classMetadata = $classMetadata;
$this->singleId = 1 === \count($ids);
$this->intId = $this->singleId && \in_array($idType, array('integer', 'smallint', 'bigint'));
$this->idField = current($ids);
// single field association are resolved, since the schema column could be an int
if ($this->singleId && $classMetadata->hasAssociation($this->idField)) {
$this->associationIdReader = new self($om, $om->getClassMetadata(
$classMetadata->getAssociationTargetClass($this->idField)
));
$this->singleId = $this->associationIdReader->isSingleId();
$this->intId = $this->associationIdReader->isIntId();
}
}
/**
* Returns whether the class has a single-column ID.
*
* @return bool returns `true` if the class has a single-column ID and
* `false` otherwise
*/
public function isSingleId()
{
return $this->singleId;
}
/**
* Returns whether the class has a single-column integer ID.
*
* @return bool returns `true` if the class has a single-column integer ID
* and `false` otherwise
*/
public function isIntId()
{
return $this->intId;
}
/**
* Returns the ID value for an object.
*
* This method assumes that the object has a single-column ID.
*
* @param object $object The object
*
* @return mixed The ID value
*/
public function getIdValue($object)
{
if (!$object) {
return;
}
if (!$this->om->contains($object)) {
throw new RuntimeException(sprintf('Entity of type "%s" passed to the choice field must be managed. Maybe you forget to persist it in the entity manager?', \get_class($object)));
}
$this->om->initializeObject($object);
$idValue = current($this->classMetadata->getIdentifierValues($object));
if ($this->associationIdReader) {
$idValue = $this->associationIdReader->getIdValue($idValue);
}
return $idValue;
}
/**
* Returns the name of the ID field.
*
* This method assumes that the object has a single-column ID.
*
* @return string The name of the ID field
*/
public function getIdField()
{
return $this->idField;
}
}

View File

@@ -0,0 +1,128 @@
<?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\ChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Loads entities using a {@link QueryBuilder} instance.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ORMQueryBuilderLoader implements EntityLoaderInterface
{
/**
* Contains the query builder that builds the query for fetching the
* entities.
*
* This property should only be accessed through queryBuilder.
*
* @var QueryBuilder
*/
private $queryBuilder;
/**
* Construct an ORM Query Builder Loader.
*
* @param QueryBuilder|\Closure $queryBuilder The query builder or a closure
* for creating the query builder.
* Passing a closure is
* deprecated and will not be
* supported anymore as of
* Symfony 3.0.
* @param ObjectManager $manager Deprecated
* @param string $class Deprecated
*
* @throws UnexpectedTypeException
*/
public function __construct($queryBuilder, $manager = null, $class = null)
{
// If a query builder was passed, it must be a closure or QueryBuilder
// instance
if (!($queryBuilder instanceof QueryBuilder || $queryBuilder instanceof \Closure)) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
}
if ($queryBuilder instanceof \Closure) {
@trigger_error('Passing a QueryBuilder closure to '.__CLASS__.'::__construct() is deprecated since Symfony 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
if (!$manager instanceof ObjectManager) {
throw new UnexpectedTypeException($manager, 'Doctrine\Common\Persistence\ObjectManager');
}
@trigger_error('Passing an EntityManager to '.__CLASS__.'::__construct() is deprecated since Symfony 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
@trigger_error('Passing a class to '.__CLASS__.'::__construct() is deprecated since Symfony 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
$queryBuilder = $queryBuilder($manager->getRepository($class));
if (!$queryBuilder instanceof QueryBuilder) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
}
}
$this->queryBuilder = $queryBuilder;
}
/**
* {@inheritdoc}
*/
public function getEntities()
{
return $this->queryBuilder->getQuery()->execute();
}
/**
* {@inheritdoc}
*/
public function getEntitiesByIds($identifier, array $values)
{
$qb = clone $this->queryBuilder;
$alias = current($qb->getRootAliases());
$parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier;
$parameter = str_replace('.', '_', $parameter);
$where = $qb->expr()->in($alias.'.'.$identifier, ':'.$parameter);
// Guess type
$entity = current($qb->getRootEntities());
$metadata = $qb->getEntityManager()->getClassMetadata($entity);
if (\in_array($metadata->getTypeOfField($identifier), array('integer', 'bigint', 'smallint'))) {
$parameterType = Connection::PARAM_INT_ARRAY;
// Filter out non-integer values (e.g. ""). If we don't, some
// databases such as PostgreSQL fail.
$values = array_values(array_filter($values, function ($v) {
return (string) $v === (string) (int) $v || ctype_digit($v);
}));
} elseif (\in_array($metadata->getTypeOfField($identifier), array('uuid', 'guid'))) {
$parameterType = Connection::PARAM_STR_ARRAY;
// Like above, but we just filter out empty strings.
$values = array_values(array_filter($values, function ($v) {
return '' !== (string) $v;
}));
} else {
$parameterType = Connection::PARAM_STR_ARRAY;
}
if (!$values) {
return array();
}
return $qb->andWhere($where)
->getQuery()
->setParameter($parameter, $values, $parameterType)
->getResult();
}
}

View File

@@ -0,0 +1,67 @@
<?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\DataTransformer;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CollectionToArrayTransformer implements DataTransformerInterface
{
/**
* Transforms a collection into an array.
*
* @return mixed An array of entities
*
* @throws TransformationFailedException
*/
public function transform($collection)
{
if (null === $collection) {
return array();
}
// For cases when the collection getter returns $collection->toArray()
// in order to prevent modifications of the returned collection
if (\is_array($collection)) {
return $collection;
}
if (!$collection instanceof Collection) {
throw new TransformationFailedException('Expected a Doctrine\Common\Collections\Collection object.');
}
return $collection->toArray();
}
/**
* Transforms choice keys into entities.
*
* @param mixed $array An array of entities
*
* @return Collection A collection of entities
*/
public function reverseTransform($array)
{
if ('' === $array || null === $array) {
$array = array();
} else {
$array = (array) $array;
}
return new ArrayCollection($array);
}
}

View File

@@ -0,0 +1,49 @@
<?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;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class DoctrineOrmExtension extends AbstractExtension
{
protected $registry;
private $propertyAccessor;
private $choiceListFactory;
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
}
protected function loadTypes()
{
return array(
new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory),
);
}
protected function loadTypeGuesser()
{
return new DoctrineOrmTypeGuesser($this->registry);
}
}

View File

@@ -0,0 +1,173 @@
<?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;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Common\Persistence\Mapping\MappingException;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException as LegacyMappingException;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\Guess\ValueGuess;
class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface
{
protected $registry;
private $cache = array();
public function __construct(ManagerRegistry $registry)
{
$this->registry = $registry;
}
/**
* {@inheritdoc}
*/
public function guessType($class, $property)
{
if (!$ret = $this->getMetadata($class)) {
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', array(), Guess::LOW_CONFIDENCE);
}
list($metadata, $name) = $ret;
if ($metadata->hasAssociation($property)) {
$multiple = $metadata->isCollectionValuedAssociation($property);
$mapping = $metadata->getAssociationMapping($property);
return new TypeGuess('Symfony\Bridge\Doctrine\Form\Type\EntityType', array('em' => $name, 'class' => $mapping['targetEntity'], 'multiple' => $multiple), Guess::HIGH_CONFIDENCE);
}
switch ($metadata->getTypeOfField($property)) {
case Type::TARRAY:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', array(), Guess::MEDIUM_CONFIDENCE);
case Type::BOOLEAN:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', array(), Guess::HIGH_CONFIDENCE);
case Type::DATETIME:
case Type::DATETIMETZ:
case 'vardatetime':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', array(), Guess::HIGH_CONFIDENCE);
case Type::DATE:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', array(), Guess::HIGH_CONFIDENCE);
case Type::TIME:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', array(), Guess::HIGH_CONFIDENCE);
case Type::DECIMAL:
case Type::FLOAT:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', array(), Guess::MEDIUM_CONFIDENCE);
case Type::INTEGER:
case Type::BIGINT:
case Type::SMALLINT:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', array(), Guess::MEDIUM_CONFIDENCE);
case Type::STRING:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', array(), Guess::MEDIUM_CONFIDENCE);
case Type::TEXT:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextareaType', array(), Guess::MEDIUM_CONFIDENCE);
default:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', array(), Guess::LOW_CONFIDENCE);
}
}
/**
* {@inheritdoc}
*/
public function guessRequired($class, $property)
{
$classMetadatas = $this->getMetadata($class);
if (!$classMetadatas) {
return;
}
/** @var ClassMetadataInfo $classMetadata */
$classMetadata = $classMetadatas[0];
// Check whether the field exists and is nullable or not
if (isset($classMetadata->fieldMappings[$property])) {
if (!$classMetadata->isNullable($property) && Type::BOOLEAN !== $classMetadata->getTypeOfField($property)) {
return new ValueGuess(true, Guess::HIGH_CONFIDENCE);
}
return new ValueGuess(false, Guess::MEDIUM_CONFIDENCE);
}
// Check whether the association exists, is a to-one association and its
// join column is nullable or not
if ($classMetadata->isAssociationWithSingleJoinColumn($property)) {
$mapping = $classMetadata->getAssociationMapping($property);
if (!isset($mapping['joinColumns'][0]['nullable'])) {
// The "nullable" option defaults to true, in that case the
// field should not be required.
return new ValueGuess(false, Guess::HIGH_CONFIDENCE);
}
return new ValueGuess(!$mapping['joinColumns'][0]['nullable'], Guess::HIGH_CONFIDENCE);
}
}
/**
* {@inheritdoc}
*/
public function guessMaxLength($class, $property)
{
$ret = $this->getMetadata($class);
if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) {
$mapping = $ret[0]->getFieldMapping($property);
if (isset($mapping['length'])) {
return new ValueGuess($mapping['length'], Guess::HIGH_CONFIDENCE);
}
if (\in_array($ret[0]->getTypeOfField($property), array(Type::DECIMAL, Type::FLOAT))) {
return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
}
}
}
/**
* {@inheritdoc}
*/
public function guessPattern($class, $property)
{
$ret = $this->getMetadata($class);
if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) {
if (\in_array($ret[0]->getTypeOfField($property), array(Type::DECIMAL, Type::FLOAT))) {
return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
}
}
}
protected function getMetadata($class)
{
// normalize class name
$class = ClassUtils::getRealClass(ltrim($class, '\\'));
if (array_key_exists($class, $this->cache)) {
return $this->cache[$class];
}
$this->cache[$class] = null;
foreach ($this->registry->getManagers() as $name => $em) {
try {
return $this->cache[$class] = array($em->getClassMetadata($class), $name);
} catch (MappingException $e) {
// not an entity or mapped super class
} catch (LegacyMappingException $e) {
// not an entity or mapped super class, using Doctrine ORM 2.2
}
}
}
}

View File

@@ -0,0 +1,48 @@
<?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\EventListener;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* Merge changes from the request to a Doctrine\Common\Collections\Collection instance.
*
* This works with ORM, MongoDB and CouchDB instances of the collection interface.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see Collection
*/
class MergeDoctrineCollectionListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
// Higher priority than core MergeCollectionListener so that this one
// is called before
return array(FormEvents::SUBMIT => array('onBind', 10));
}
public function onBind(FormEvent $event)
{
$collection = $event->getForm()->getData();
$data = $event->getData();
// If all items were removed, call clear which has a higher
// performance on persistent collections
if ($collection instanceof Collection && 0 === \count($data)) {
$collection->clear();
}
}
}

View File

@@ -0,0 +1,338 @@
<?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\Persistence\ManagerRegistry;
use Doctrine\Common\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\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
abstract class DoctrineType extends AbstractType
{
/**
* @var ManagerRegistry
*/
protected $registry;
/**
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
/**
* @var IdReader[]
*/
private $idReaders = array();
/**
* @var DoctrineChoiceLoader[]
*/
private $choiceLoaders = array();
/**
* Creates the label for a choice.
*
* For backwards compatibility, objects are cast to strings by default.
*
* @param object $choice The object
*
* @return string The string representation of the object
*
* @internal This method is public to be usable as callback. It should not
* be used in user code.
*/
public static function createChoiceLabel($choice)
{
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 object $choice The object
* @param int|string $key The choice key
* @param string $value The choice value. Corresponds to the object's
* ID here.
*
* @return string The field name
*
* @internal This method is public to be usable as callback. It should not
* be used in user code.
*/
public static function createChoiceName($choice, $key, $value)
{
return str_replace('-', '_', (string) $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
*
* @return array|false Array with important QueryBuilder parts or false if
* they can't be determined
*
* @internal This method is public to be usable as callback. It should not
* be used in user code.
*/
public function getQueryBuilderPartsForCachingHash($queryBuilder)
{
return false;
}
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->registry = $registry;
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
new PropertyAccessDecorator(
new DefaultChoiceListFactory(),
$propertyAccessor
)
);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['multiple']) {
$builder
->addEventSubscriber(new MergeDoctrineCollectionListener())
->addViewTransformer(new CollectionToArrayTransformer(), true)
;
}
}
public function configureOptions(OptionsResolver $resolver)
{
$registry = $this->registry;
$choiceListFactory = $this->choiceListFactory;
$idReaders = &$this->idReaders;
$choiceLoaders = &$this->choiceLoaders;
$type = $this;
$choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) {
// Unless the choices are given explicitly, load them on demand
if (null === $options['choices']) {
$hash = null;
$qbParts = null;
// If there is no QueryBuilder we can safely cache DoctrineChoiceLoader,
// also if concrete Type can return important QueryBuilder parts to generate
// hash key we go for it as well
if (!$options['query_builder'] || false !== ($qbParts = $type->getQueryBuilderPartsForCachingHash($options['query_builder']))) {
$hash = CachingFactoryDecorator::generateHash(array(
$options['em'],
$options['class'],
$qbParts,
$options['loader'],
));
if (isset($choiceLoaders[$hash])) {
return $choiceLoaders[$hash];
}
}
if ($options['loader']) {
$entityLoader = $options['loader'];
} elseif (null !== $options['query_builder']) {
$entityLoader = $type->getLoader($options['em'], $options['query_builder'], $options['class']);
} else {
$queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e');
$entityLoader = $type->getLoader($options['em'], $queryBuilder, $options['class']);
}
$doctrineChoiceLoader = new DoctrineChoiceLoader(
$choiceListFactory,
$options['em'],
$options['class'],
$options['id_reader'],
$entityLoader
);
if (null !== $hash) {
$choiceLoaders[$hash] = $doctrineChoiceLoader;
}
return $doctrineChoiceLoader;
}
};
$choiceLabel = function (Options $options) {
// BC with the "property" option
if ($options['property']) {
return $options['property'];
}
// BC: use __toString() by default
return array(__CLASS__, 'createChoiceLabel');
};
$choiceName = function (Options $options) {
/** @var IdReader $idReader */
$idReader = $options['id_reader'];
// 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 name
if ($idReader->isIntId()) {
return array(__CLASS__, 'createChoiceName');
}
// Otherwise, an incrementing integer is used as name automatically
};
// 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) {
/** @var IdReader $idReader */
$idReader = $options['id_reader'];
// If the entity has a single-column ID, use that ID as value
if ($idReader->isSingleId()) {
return array($idReader, 'getIdValue');
}
// Otherwise, an incrementing integer is used as value automatically
};
$emNormalizer = function (Options $options, $em) use ($registry) {
/* @var ManagerRegistry $registry */
if (null !== $em) {
if ($em instanceof ObjectManager) {
return $em;
}
return $registry->getManager($em);
}
$em = $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;
};
// deprecation note
$propertyNormalizer = function (Options $options, $propertyName) {
if ($propertyName) {
@trigger_error('The "property" option is deprecated since Symfony 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED);
}
return $propertyName;
};
// 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 = \call_user_func($queryBuilder, $options['em']->getRepository($options['class']));
}
return $queryBuilder;
};
// deprecation note
$loaderNormalizer = function (Options $options, $loader) {
if ($loader) {
@trigger_error('The "loader" option is deprecated since Symfony 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED);
}
return $loader;
};
// Set the "id_reader" option via the normalizer. This option is not
// supposed to be set by the user.
$idReaderNormalizer = function (Options $options) use (&$idReaders) {
$hash = CachingFactoryDecorator::generateHash(array(
$options['em'],
$options['class'],
));
// 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.
if (!isset($idReaders[$hash])) {
$classMetadata = $options['em']->getClassMetadata($options['class']);
$idReaders[$hash] = new IdReader($options['em'], $classMetadata);
}
return $idReaders[$hash];
};
$resolver->setDefaults(array(
'em' => null,
'property' => null, // deprecated, use "choice_label"
'query_builder' => null,
'loader' => null, // deprecated, use "choice_loader"
'choices' => null,
'choices_as_values' => true,
'choice_loader' => $choiceLoader,
'choice_label' => $choiceLabel,
'choice_name' => $choiceName,
'choice_value' => $choiceValue,
'id_reader' => null, // internal
'choice_translation_domain' => false,
));
$resolver->setRequired(array('class'));
$resolver->setNormalizer('em', $emNormalizer);
$resolver->setNormalizer('property', $propertyNormalizer);
$resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
$resolver->setNormalizer('loader', $loaderNormalizer);
$resolver->setNormalizer('id_reader', $idReaderNormalizer);
$resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager'));
$resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'));
}
/**
* Return the default loader object.
*
* @param ObjectManager $manager
* @param mixed $queryBuilder
* @param string $class
*
* @return EntityLoaderInterface
*/
abstract public function getLoader(ObjectManager $manager, $queryBuilder, $class);
public function getParent()
{
return 'Symfony\Component\Form\Extension\Core\Type\ChoiceType';
}
}

View File

@@ -0,0 +1,104 @@
<?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\Persistence\ObjectManager;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class EntityType extends DoctrineType
{
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
// 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 = \call_user_func($queryBuilder, $options['em']->getRepository($options['class']));
if (null !== $queryBuilder && !$queryBuilder instanceof QueryBuilder) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
}
}
return $queryBuilder;
};
$resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
$resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder'));
}
/**
* Return the default loader object.
*
* @param ObjectManager $manager
* @param QueryBuilder $queryBuilder
* @param string $class
*
* @return ORMQueryBuilderLoader
*/
public function getLoader(ObjectManager $manager, $queryBuilder, $class)
{
return new ORMQueryBuilderLoader($queryBuilder, $manager, $class);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'entity';
}
/**
* We consider two query builders with an equal SQL string and
* equal parameters to be equal.
*
* @param QueryBuilder $queryBuilder
*
* @return array
*
* @internal This method is public to be usable as callback. It should not
* be used in user code.
*/
public function getQueryBuilderPartsForCachingHash($queryBuilder)
{
return array(
$queryBuilder->getQuery()->getSQL(),
array_map(array($this, 'parameterToArray'), $queryBuilder->getParameters()->toArray()),
);
}
/**
* Converts a query parameter to an array.
*
* @return array The array representation of the parameter
*/
private function parameterToArray(Parameter $parameter)
{
return array($parameter->getName(), $parameter->getType(), $parameter->getValue());
}
}