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 all 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
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ sudo: false

matrix:
include:
- php: 5.3
env: SYMFONY_VERSION=2.3.* COMPOSER_FLAGS="--prefer-lowest"
- php: 5.6
env: SYMFONY_VERSION=2.3.* SYMFONY_DEPRECATIONS_HELPER=weak
- php: 5.6
env: SYMFONY_VERSION=2.8.*
- php: 5.6
Expand Down
124 changes: 124 additions & 0 deletions Form/ChoiceList/DoctrineChoiceLoader.php
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
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;

/**
* @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);
}
}
21 changes: 20 additions & 1 deletion Form/ChoiceList/PhpcrOdmQueryBuilderLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Doctrine\Bundle\PHPCRBundle\Form\ChoiceList;

use Doctrine\ODM\PHPCR\DocumentManager;
use Doctrine\ODM\PHPCR\Mapping\ClassMetadata;
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 All @@ -26,6 +28,11 @@ class PhpcrOdmQueryBuilderLoader implements EntityLoaderInterface
*/
private $queryBuilder;

/**
* @var ClassMetadata
*/
private $classMetadata;

/**
* Construct a PHPCR-ODM Query Builder Loader.
*
Expand All @@ -52,6 +59,13 @@ public function __construct($queryBuilder, DocumentManager $manager = null, $cla
$this->manager = $manager;

$this->queryBuilder = $queryBuilder;

if (null !== $class) {
$classMetadata = $this->manager->getClassMetadata($class);
if ($classMetadata instanceof ClassMetadata) {
$this->classMetadata = $classMetadata;
}
}
}

/**
Expand Down Expand Up @@ -93,8 +107,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 ($this->classMetadata && $this->classMetadata->referenceable && UUIDHelper::isUUID($val)) {
$where->eq()->field($alias.'.'.$this->classMetadata->getUuidFieldName())->literal($val)->end();
} else {
$where->same($val, $alias);
}
}

return $this->getResult($qb);
Expand Down
90 changes: 90 additions & 0 deletions Form/Type/DocumentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand All @@ -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)
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']);
}

// 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}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ public function setUp()

public function testGetByIds()
{
$qb = $this->dm->getRepository('Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument')->createQueryBuilder('e');
$loader = new PhpcrOdmQueryBuilderLoader($qb, $this->dm);
$class = 'Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument';
$qb = $this->dm->getRepository($class)->createQueryBuilder('e');
$loader = new PhpcrOdmQueryBuilderLoader($qb, $this->dm, $class);
$ids = array('/test/doc', '/test/doc2', '/test/doc3');
$documents = $loader->getEntitiesByIds('id', $ids);
$this->assertCount(2, $documents);
Expand All @@ -35,19 +36,38 @@ public function testGetByIds()
}
}

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

$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');
$loader = new PhpcrOdmQueryBuilderLoader($qb, $this->dm);
$class = 'Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument';
$qb = $this->dm->getRepository($class)->createQueryBuilder('e');
$loader = new PhpcrOdmQueryBuilderLoader($qb, $this->dm, $class);
$documents = $loader->getEntitiesByIds('id', array('/foo/bar'));
$this->assertCount(0, $documents);
}

public function testGetByIdsFilter()
{
$class = 'Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument';
$qb = $this->dm->getRepository('Doctrine\Bundle\PHPCRBundle\Tests\Resources\Document\TestDocument')->createQueryBuilder('e');
$qb->where()->eq()->field('e.text')->literal('thiswillnotmatch');
$loader = new PhpcrOdmQueryBuilderLoader($qb, $this->dm);
$loader = new PhpcrOdmQueryBuilderLoader($qb, $this->dm, $class);
$documents = $loader->getEntitiesByIds('id', array('/test/doc'));
$this->assertCount(0, $documents);
}
Expand Down
Loading