Skip to content
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

Open
wants to merge 14 commits into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions Form/ChoiceList/DoctrineChoiceLoader.php
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
Copy link
Member

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?

{
/**
* @var ChoiceListFactoryInterface
*/
private $factory;

/**
* @var ObjectManager
*/
private $manager;

/**
* @var string
*/
private $class;

/**
* @var IdReader
*/
private $idReader;

/**
* @var null|EntityLoaderInterface
Copy link
Contributor

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. ;-)

*/
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);
}
}
10 changes: 8 additions & 2 deletions Form/ChoiceList/PhpcrOdmQueryBuilderLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -82,7 +83,7 @@ public function getEntitiesByIds($identifier, array $values)
}));

if (0 == count($values)) {
return array();
return [];
Copy link
Member

Choose a reason for hiding this comment

The 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 "
Expand All @@ -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();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I force it to be on the uuid property of the Document. Since the class passed in the constructor is sometimes null, I cant use its metadata. Any ideas on how we could get the propert metadata here?

Copy link
Member

Choose a reason for hiding this comment

The 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 $uuid? this is an assumption we should not make. maybe we just need an option to specify the uuid property to use with this form type, and if it is not set, we assume path?

Copy link
Author

Choose a reason for hiding this comment

The 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 PhpcrOdmQueryBuilderLoaderTest.php, there the $class is never passed, so I could fix this to add a class to the 3rth argument of its constructor.

} else {
$where->same($val, $alias);
}
}

return $this->getResult($qb);
Expand Down
85 changes: 85 additions & 0 deletions Form/Type/DocumentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand All @@ -34,6 +57,68 @@ public function getLoader(ObjectManager $manager, $queryBuilder, $class)
);
}

public function configureOptions(OptionsResolver $resolver)
Copy link
Member

Choose a reason for hiding this comment

The 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) {
Copy link
Member

Choose a reason for hiding this comment

The 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}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ public function testGetByIds()
}
}

public function testGetByIdsWithUuid()
{
$qb = $this->dm->getRepository('Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument')->createQueryBuilder('e');
$loader = new PhpcrOdmQueryBuilderLoader($qb, $this->dm);

$doc = $this->dm->find(null, '/test/doc');
$uuids = array($doc->uuid, 'non-existing-uuid');

$documents = $loader->getEntitiesByIds('uuid', $uuids);
$this->assertCount(1, $documents);
foreach ($documents as $i => $document) {
$this->assertInstanceOf('Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument', $document);
$this->assertTrue(in_array($document->uuid, $uuids));
}
}

public function testGetByIdsNotFound()
{
$qb = $this->dm->getRepository('Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument')->createQueryBuilder('e');
Expand Down
Loading