-
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 all 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,124 @@ | ||
<?php | ||
|
||
namespace Doctrine\Bundle\PHPCRBundle\Form\ChoiceList; | ||
|
||
use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader as BaseDoctrineChoiceLoader; | ||
use Doctrine\Common\Persistence\ObjectManager; | ||
use Doctrine\ODM\PHPCR\Mapping\ClassMetadata as PHPCRClassMetadata; | ||
use Doctrine\Common\Persistence\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\PropertyAccess\PropertyAccess; | ||
use Symfony\Component\PropertyAccess\PropertyAccessor; | ||
|
||
/** | ||
* Supports UUIDs as choice values, it will automatically detect them and if the PHPCR document | ||
* has a UUID field mapping it will be enabled. | ||
* | ||
* @author Steffen Brem <steffenbrem@gmail.com> | ||
*/ | ||
class DoctrineChoiceLoader extends BaseDoctrineChoiceLoader | ||
{ | ||
/** | ||
* @var ObjectManager | ||
*/ | ||
private $manager; | ||
|
||
/** | ||
* @var ClassMetadata | ||
*/ | ||
private $classMetadata; | ||
|
||
/** | ||
* @var IdReader | ||
*/ | ||
private $idReader; | ||
|
||
/** | ||
* @var null|EntityLoaderInterface | ||
*/ | ||
private $objectLoader; | ||
|
||
/** | ||
* @var ChoiceListInterface | ||
*/ | ||
private $choiceList; | ||
|
||
/** | ||
* @var PropertyAccessor | ||
*/ | ||
private $propertyAccessor; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader = null, EntityLoaderInterface $objectLoader = null) | ||
{ | ||
parent::__construct($factory, $manager, $class, $idReader, $objectLoader); | ||
|
||
$classMetadata = $manager->getClassMetadata($class); | ||
|
||
$this->manager = $manager; | ||
$this->classMetadata = $classMetadata; | ||
$this->idReader = $idReader ?: new IdReader($manager, $classMetadata); | ||
$this->objectLoader = $objectLoader; | ||
$this->propertyAccessor = PropertyAccess::createPropertyAccessor(); | ||
} | ||
|
||
/** | ||
* {@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(); | ||
} | ||
|
||
$uuidFieldName = null; | ||
if ($this->classMetadata instanceof PHPCRClassMetadata && $this->classMetadata->referenceable) { | ||
$uuidFieldName = $this->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 (null !== $uuidFieldName && UUIDHelper::isUUID($id)) { | ||
foreach ($unorderedObjects as $object) { | ||
if ($id === $this->propertyAccessor->getValue($object, $uuidFieldName)) { | ||
$objects[$i] = $object; | ||
break; | ||
} | ||
} | ||
} elseif (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 |
---|---|---|
|
@@ -16,12 +16,34 @@ | |
|
||
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 +56,74 @@ public function getLoader(ObjectManager $manager, $queryBuilder, $class) | |
); | ||
} | ||
|
||
/** | ||
* We need to overrive the `choice_loader`, so that we can use our own DoctrineChoiceLoader | ||
* class that includes support for UUIDs. | ||
* | ||
* @param OptionsResolver $resolver | ||
*/ | ||
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']); | ||
} | ||
|
||
// this line is different from the original choice loader, we use our own DoctrineChoiceLoader, | ||
// were we have our changes to support UUIDs | ||
$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.
EntityLoaderInterface|null
Just a detail, but this will keep code consistency. ;-)