-
Notifications
You must be signed in to change notification settings - Fork 66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Support UUID as a choice value for DocumentType #236
base: 3.x
Are you sure you want to change the base?
Changes from 5 commits
5d97c8a
8c6ed79
2d6df56
7754514
e544a55
386103b
dbb7196
d046e76
66a9895
b3846f7
43f65ad
d794f90
42a04e4
a2a1446
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
<?php | ||
/** | ||
* (c) Steffen Brem <steffenbrem@gmail.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Doctrine\Bundle\PHPCRBundle\Form\ChoiceList; | ||
|
||
use Doctrine\Common\Persistence\ObjectManager; | ||
use Doctrine\ODM\PHPCR\Mapping\ClassMetadata; | ||
use PHPCR\Util\UUIDHelper; | ||
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; | ||
use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; | ||
use Symfony\Component\Form\ChoiceList\ChoiceListInterface; | ||
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; | ||
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; | ||
use Symfony\Component\PropertyAccess\PropertyAccess; | ||
|
||
/** | ||
* DoctrineChoiceLoader | ||
* | ||
* @author Steffen Brem <steffenbrem@gmail.com> | ||
*/ | ||
class DoctrineChoiceLoader implements ChoiceLoaderInterface | ||
{ | ||
/** | ||
* @var ChoiceListFactoryInterface | ||
*/ | ||
private $factory; | ||
|
||
/** | ||
* @var ObjectManager | ||
*/ | ||
private $manager; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $class; | ||
|
||
/** | ||
* @var IdReader | ||
*/ | ||
private $idReader; | ||
|
||
/** | ||
* @var null|EntityLoaderInterface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Just a detail, but this will keep code consistency. ;-) |
||
*/ | ||
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 null|EntityLoaderInterface $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; | ||
|
||
$this->propertyAccessor = PropertyAccess::createPropertyAccessor(); | ||
} | ||
|
||
/** | ||
* {@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 | ||
|
||
// Attention: This optimization does not check choices for existence | ||
if (!$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(); | ||
} | ||
|
||
$classMetadata = $this->manager->getClassMetadata($this->class); | ||
|
||
$uuidFieldName = null; | ||
|
||
if ($classMetadata instanceof ClassMetadata) { | ||
if ($classMetadata->referenceable) { | ||
$uuidFieldName = $classMetadata->getUuidFieldName(); | ||
} | ||
} | ||
|
||
// Optimize performance in case we have an object loader and | ||
// a single-field identifier | ||
if (!$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[$this->idReader->getIdValue($object)] = $object; | ||
} | ||
|
||
foreach ($values as $i => $id) { | ||
if (UUIDHelper::isUUID($id)) { | ||
foreach ($unorderedObjects as $object) { | ||
if ($id === $this->propertyAccessor->getValue($object, $uuidFieldName)) { | ||
$objects[$i] = $object; | ||
break; | ||
} | ||
} | ||
} else if (isset($objectsById[$id])) { | ||
$objects[$i] = $objectsById[$id]; | ||
} | ||
} | ||
|
||
return $objects; | ||
} | ||
|
||
return $this->loadChoiceList($value)->getChoicesForValues($values); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
|
||
use Doctrine\ODM\PHPCR\DocumentManager; | ||
use Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder; | ||
use PHPCR\Util\UUIDHelper; | ||
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; | ||
use Symfony\Component\Form\Exception\UnexpectedTypeException; | ||
|
||
|
@@ -82,7 +83,7 @@ public function getEntitiesByIds($identifier, array $values) | |
})); | ||
|
||
if (0 == count($values)) { | ||
return array(); | ||
return []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is only valid with php 5.4+. lets not change that yet, but when we go 2.0 and drop 5.3 support. |
||
} | ||
|
||
/* performance: if we could figure out whether the query builder is " | ||
|
@@ -93,8 +94,13 @@ public function getEntitiesByIds($identifier, array $values) | |
$qb = clone $this->queryBuilder; | ||
$alias = $qb->getPrimaryAlias(); | ||
$where = $qb->andWhere()->orX(); | ||
|
||
foreach ($values as $val) { | ||
$where->same($val, $alias); | ||
if (UUIDHelper::isUUID($val)) { | ||
$where->eq()->field($alias.'.uuid')->literal($val)->end(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I force it to be on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't have the model instance at this point yet, right? could we find the class from the query builder if its not explicitly specified? btw, is this assuming that the uuid is mapped on the property There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dbu The class is always in the query builder, but i think we need to use reflection in order to get the class out of it. The issue is more with the tests in |
||
} else { | ||
$where->same($val, $alias); | ||
} | ||
} | ||
|
||
return $this->getResult($qb); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,12 +16,35 @@ | |
|
||
namespace Doctrine\Bundle\PHPCRBundle\Form\Type; | ||
|
||
use Doctrine\Common\Persistence\ManagerRegistry; | ||
use Doctrine\Common\Persistence\ObjectManager; | ||
use Doctrine\Bundle\PHPCRBundle\Form\ChoiceList\DoctrineChoiceLoader; | ||
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\OptionsResolver\Options; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||
|
||
use Doctrine\Bundle\PHPCRBundle\Form\ChoiceList\PhpcrOdmQueryBuilderLoader; | ||
use Symfony\Bridge\Doctrine\Form\Type\DoctrineType; | ||
|
||
class DocumentType extends DoctrineType | ||
{ | ||
private $choiceListFactory; | ||
|
||
/** | ||
* @var DoctrineChoiceLoader[] | ||
*/ | ||
private $choiceLoaders = array(); | ||
|
||
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) | ||
{ | ||
parent::__construct($registry, $propertyAccessor, $choiceListFactory); | ||
$this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory(), $propertyAccessor); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
|
@@ -34,6 +57,68 @@ public function getLoader(ObjectManager $manager, $queryBuilder, $class) | |
); | ||
} | ||
|
||
public function configureOptions(OptionsResolver $resolver) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a phpdoc comment explaining why we need to overwrite this? |
||
{ | ||
parent::configureOptions($resolver); | ||
|
||
$choiceListFactory = $this->choiceListFactory; | ||
$choiceLoaders = &$this->choiceLoaders; | ||
$type = $this; | ||
|
||
$choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a comment on the lines where we differ from the orm loader to explain the differences? will make future maintenance easier. |
||
|
||
// 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 ($hash !== null) { | ||
$choiceLoaders[$hash] = $doctrineChoiceLoader; | ||
} | ||
|
||
return $doctrineChoiceLoader; | ||
} | ||
}; | ||
|
||
|
||
$resolver->setDefaults(array( | ||
'choice_loader' => $choiceLoader | ||
)); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really need to duplicate the whole class? could we extend it and just change what is actually different?