Skip to content

Commit

Permalink
Specify unique node types via mapping to allow proper outer joins -- d…
Browse files Browse the repository at this point in the history
  • Loading branch information
sarcher committed Nov 6, 2015
1 parent 2edc458 commit 0b64134
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 7 deletions.
5 changes: 5 additions & 0 deletions lib/Doctrine/ODM/PHPCR/Mapping/Annotations/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,9 @@ class Document
* @var boolean
*/
public $referenceable;

/**
* @var boolean
*/
public $uniqueNodeType;
}
29 changes: 29 additions & 0 deletions lib/Doctrine/ODM/PHPCR/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ class ClassMetadata implements ClassMetadataInterface
*/
public $referenceable = false;

/**
* READ-ONLY: If true, consider this document's node type to be unique among all mappings.
*
* @var bool
*/
public $uniqueNodeType = false;

/**
* READ-ONLY: Strategy key to find field translations.
* This is the key used for DocumentManagerInterface::getTranslationStrategy
Expand Down Expand Up @@ -611,6 +618,24 @@ public function setReferenceable($referenceable)
$this->referenceable = $referenceable;
}

/**
* @param bool $uniqueNodeType
*/
public function setUniqueNodeType($uniqueNodeType)
{
$this->uniqueNodeType = $uniqueNodeType;
}

/**
* Return true if this document has a unique node type among all mappings.
*
* @return bool
*/
public function hasUniqueNodeType()
{
return $this->uniqueNodeType;
}

/**
* @param string $nodeType
*/
Expand Down Expand Up @@ -1481,6 +1506,10 @@ public function __sleep()
$serialized[] = 'referenceable';
}

if ($this->uniqueNodeType) {
$serialized[] = 'uniqueNodeType';
}

if ($this->lifecycleCallbacks) {
$serialized[] = 'lifecycleCallbacks';
}
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/ODM/PHPCR/Mapping/Driver/AnnotationDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
$metadata->setVersioned($documentAnnot->versionable);
}

if (null !== $documentAnnot->uniqueNodeType) {
$metadata->setUniqueNodeType($documentAnnot->uniqueNodeType);
}

if (null !== $documentAnnot->mixins) {
$metadata->setMixins($documentAnnot->mixins);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/ODM/PHPCR/Mapping/Driver/XmlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ public function loadMetadataForClass($className, ClassMetadata $class)
$class->setReferenceable((bool) $xmlRoot['referenceable']);
}

if (isset($xmlRoot['uniqueNodeType']) && $xmlRoot['uniqueNodeType'] !== 'false') {
$class->setUniqueNodeType((bool) $xmlRoot['uniqueNodeType']);
}

if (isset($xmlRoot->mixins)) {
$mixins = array();
foreach ($xmlRoot->mixins->mixin as $mixin) {
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/ODM/PHPCR/Mapping/Driver/YamlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public function loadMetadataForClass($className, ClassMetadata $class)
$class->setReferenceable($element['referenceable']);
}

if (isset($element['uniqueNodeType']) && $element['uniqueNodeType']) {
$class->setUniqueNodeType($element['uniqueNodeType']);
}

if (isset($element['mixins'])) {
$mixins = array();
foreach ($element['mixins'] as $mixin) {
Expand Down
19 changes: 14 additions & 5 deletions lib/Doctrine/ODM/PHPCR/Query/Builder/ConverterPhpcr.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ public function getQuery(QueryBuilder $builder)
);
}

// be explicit in what we select
if (empty($this->columns)) {
foreach (array_keys($this->sourceDocumentNodes) as $selectorName) {
$this->columns[] = $this->qomf->column($selectorName);
}
}

// for each document source add phpcr:{class,classparents} restrictions
foreach ($this->sourceDocumentNodes as $sourceNode) {
$documentFqn = $this->aliasMetadata[$sourceNode->getAlias()]->getName();
Expand Down Expand Up @@ -262,11 +269,6 @@ protected function walkSourceDocument(SourceDocument $node)
$alias = $node->getAlias();
$documentFqn = $node->getDocumentFqn();

// make sure we add the phpcr:{class,classparents} constraints
// From is dispatched first, so these will always be the primary
// constraints.
$this->sourceDocumentNodes[$alias] = $node;

// cache the metadata for this document
/** @var $meta ClassMetadata */
$meta = $this->mdf->getMetadataFor($documentFqn);
Expand All @@ -283,6 +285,13 @@ protected function walkSourceDocument(SourceDocument $node)
}
$nodeType = $meta->getNodeType();

// make sure we add the phpcr:{class,classparents} constraints
// unless the document has a unique type; From is dispatched first,
// so these will always be the primary constraints.
if (!$meta->hasUniqueNodeType()) {
$this->sourceDocumentNodes[$alias] = $node;
}

// get the PHPCR Alias
$alias = $this->qomf->selector(
$alias,
Expand Down
74 changes: 74 additions & 0 deletions tests/Doctrine/Tests/Models/CMS/CmsProfile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Doctrine\Tests\Models\CMS;

use Doctrine\ODM\PHPCR\DocumentRepository;
use Doctrine\ODM\PHPCR\Id\RepositoryIdInterface;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;

/**
* @PHPCRODM\Document(
* nodeType="phpcr:cms_profile",
* referenceable=true,
* repositoryClass="Doctrine\Tests\Models\CMS\CmsProfileRepository",
* uniqueNodeType=true
* )
*/
class CmsProfile
{
/** @PHPCRODM\Id(strategy="repository") */
public $id;

/** @PHPCRODM\Uuid */
public $uuid;

/** @PHPCRODM\Field(type="string") */
public $data;

/** @PHPCRODM\ReferenceOne(targetDocument="CmsUser", cascade="persist") */
public $user;

public function getId()
{
return $this->id;
}

public function getUuid()
{
return $this->uuid;
}

public function setData($data)
{
$this->data = $data;
}

public function getData()
{
return $this->data;
}

public function setUser(CmsUser $user)
{
$this->user = $user;
}

public function getUser()
{
return $this->user;
}
}

class CmsProfileRepository extends DocumentRepository implements RepositoryIdInterface
{
/**
* Generate a document id
*
* @param object $document
* @return string
*/
public function generateId($document, $parent = null)
{
return '/functional/' . $document->user->username . '/' . $document->data;
}
}
13 changes: 13 additions & 0 deletions tests/Doctrine/Tests/Models/CMS/CmsUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class CmsUser
public $articles;
/** @PHPCRODM\ReferenceMany(targetDocument="CmsGroup") */
public $groups;
/** @PHPCRODM\ReferenceMany(targetDocument="CmsProfile") */
public $profiles;
/** @PHPCRODM\Children() */
public $children;
/** @PHPCRODM\Child(nodeName="assistant", cascade="persist") */
Expand Down Expand Up @@ -82,6 +84,17 @@ public function getGroups()
{
return $this->groups;
}

public function addProfile(CmsProfile $profile)
{
$this->profiles[] = $profile;
$profile->setUser($this);
}

public function getProfiles()
{
return $this->profiles;
}
}

class CmsUserRepository extends DocumentRepository implements RepositoryIdInterface
Expand Down
38 changes: 38 additions & 0 deletions tests/Doctrine/Tests/ODM/PHPCR/Functional/QueryBuilderJoinTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Doctrine\ODM\PHPCR\Query\Builder\AbstractNode as QBConstants;
use Doctrine\Tests\ODM\PHPCR\PHPCRFunctionalTestCase;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsProfile;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\CMS\CmsAuditItem;

Expand All @@ -23,6 +24,9 @@ public function setUp()

$this->resetFunctionalNode($this->dm);

$ntm = $this->dm->getPhpcrSession()->getWorkspace()->getNodeTypeManager();
$ntm->registerNodeTypesCnd('[phpcr:cms_profile] > nt:unstructured', true);

$address1 = new CmsAddress;
$address1->country = 'France';
$address1->city = 'Lyon';
Expand All @@ -40,6 +44,11 @@ public function setUp()
$user->address = $address1;
$this->dm->persist($user);

$profile = new CmsProfile();
$profile->setData('testdata');
$user->addProfile($profile);
$this->dm->persist($profile);

$user = new CmsUser;
$user->username = 'winstonsmith';
$user->address = $address2;
Expand Down Expand Up @@ -198,4 +207,33 @@ public function testEquiJoinInner($joinType, $leftClass, $rightClass, $criteria
}
}
}

/**
* Verify that using an outer join on a document that is uniquely typed
* results in the full expected result set.
*/
public function testUniqueNodeTypeOuterJoin()
{
$qb = $this->dm->createQueryBuilder()
->fromDocument('Doctrine\Tests\Models\CMS\CmsUser', 'u')
->addJoinLeftOuter()
->right()->document('Doctrine\Tests\Models\CMS\CmsProfile', 'p')->end()
->condition()->equi('u.profiles', 'p.uuid')
->end()->end()
->where()->orX()
->eq()->field('u.username')->literal('winstonsmith')->end()
->eq()->field('p.data')->literal('testdata')->end()
->end()->end()
->orderBy()->asc()->field('u.username')->end()->end();

$q = $qb->getQuery();
$phpcrQuery = $q->getPhpcrQuery();
$phpcrRes = $phpcrQuery->execute();
$phpcrRows = $phpcrRes->getRows();

$this->assertCount(2, $phpcrRows);
foreach ($phpcrRows as $key => $row) {
$this->assertEquals($key == 0 ? '/functional/dantleech' : '/functional/winstonsmith', $row->getPath('u'));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,22 @@ public function testReferenceableMapping($class)
$this->assertTrue($class->referenceable);
}

public function testLoadUniqueNodeTypeMapping()
{
$className = 'Doctrine\Tests\ODM\PHPCR\Mapping\Model\UniqueNodeTypeMappingObject';

return $this->loadMetadataForClassname($className);
}

/**
* @depends testLoadUniqueNodeTypeMapping
* @param ClassMetadata $class
*/
public function testUniqueNodeTypeMapping($class)
{
$this->assertTrue($class->uniqueNodeType);
}

public function testLoadNodeTypeMapping()
{
$className = 'Doctrine\Tests\ODM\PHPCR\Mapping\Model\NodeTypeMappingObject';
Expand Down
6 changes: 4 additions & 2 deletions tests/Doctrine/Tests/ODM/PHPCR/Mapping/ClassMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,24 +224,26 @@ public function testNewInstance(ClassMetadata $cm)
*/
public function testSerialize(ClassMetadata $cm)
{
$expected = 'O:40:"Doctrine\ODM\PHPCR\Mapping\ClassMetadata":19:{s:8:"nodeType";s:15:"nt:unstructured";s:10:"identifier";s:2:"id";s:4:"name";s:39:"Doctrine\Tests\ODM\PHPCR\Mapping\Person";s:11:"idGenerator";i:2;s:8:"mappings";a:5:{s:2:"id";a:7:{s:9:"fieldName";s:2:"id";s:2:"id";b:1;s:8:"strategy";s:8:"assigned";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;s:8:"property";s:2:"id";}s:8:"username";a:5:{s:9:"fieldName";s:8:"username";s:8:"property";s:8:"username";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:7:"created";a:5:{s:9:"fieldName";s:7:"created";s:8:"property";s:7:"created";s:4:"type";s:8:"datetime";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:6:"locale";a:3:{s:9:"fieldName";s:6:"locale";s:4:"type";s:6:"locale";s:8:"property";s:6:"locale";}s:15:"translatedField";a:7:{s:9:"fieldName";s:15:"translatedField";s:10:"translated";b:1;s:8:"property";s:15:"translatedField";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:5:"assoc";N;s:8:"nullable";b:0;}}s:13:"fieldMappings";a:4:{i:0;s:2:"id";i:1;s:8:"username";i:2;s:7:"created";i:3;s:15:"translatedField";}s:17:"referenceMappings";a:0:{}s:17:"referrersMappings";a:0:{}s:22:"mixedReferrersMappings";a:0:{}s:16:"childrenMappings";a:0:{}s:13:"childMappings";a:0:{}s:25:"customRepositoryClassName";s:51:"Doctrine\Tests\ODM\PHPCR\Mapping\DocumentRepository";s:18:"isMappedSuperclass";b:1;s:11:"versionable";b:1;s:18:"lifecycleCallbacks";a:1:{s:8:"postLoad";a:1:{i:0;s:8:"callback";}}s:13:"inheritMixins";b:1;s:13:"localeMapping";s:6:"locale";s:10:"translator";s:9:"attribute";s:18:"translatableFields";a:1:{i:0;s:15:"translatedField";}}';
$expected = 'O:40:"Doctrine\ODM\PHPCR\Mapping\ClassMetadata":20:{s:8:"nodeType";s:15:"nt:unstructured";s:10:"identifier";s:2:"id";s:4:"name";s:39:"Doctrine\Tests\ODM\PHPCR\Mapping\Person";s:11:"idGenerator";i:2;s:8:"mappings";a:5:{s:2:"id";a:7:{s:9:"fieldName";s:2:"id";s:2:"id";b:1;s:8:"strategy";s:8:"assigned";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;s:8:"property";s:2:"id";}s:8:"username";a:5:{s:9:"fieldName";s:8:"username";s:8:"property";s:8:"username";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:7:"created";a:5:{s:9:"fieldName";s:7:"created";s:8:"property";s:7:"created";s:4:"type";s:8:"datetime";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:6:"locale";a:3:{s:9:"fieldName";s:6:"locale";s:4:"type";s:6:"locale";s:8:"property";s:6:"locale";}s:15:"translatedField";a:7:{s:9:"fieldName";s:15:"translatedField";s:10:"translated";b:1;s:8:"property";s:15:"translatedField";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:5:"assoc";N;s:8:"nullable";b:0;}}s:13:"fieldMappings";a:4:{i:0;s:2:"id";i:1;s:8:"username";i:2;s:7:"created";i:3;s:15:"translatedField";}s:17:"referenceMappings";a:0:{}s:17:"referrersMappings";a:0:{}s:22:"mixedReferrersMappings";a:0:{}s:16:"childrenMappings";a:0:{}s:13:"childMappings";a:0:{}s:25:"customRepositoryClassName";s:51:"Doctrine\Tests\ODM\PHPCR\Mapping\DocumentRepository";s:18:"isMappedSuperclass";b:1;s:11:"versionable";b:1;s:14:"uniqueNodeType";b:1;s:18:"lifecycleCallbacks";a:1:{s:8:"postLoad";a:1:{i:0;s:8:"callback";}}s:13:"inheritMixins";b:1;s:13:"localeMapping";s:6:"locale";s:10:"translator";s:9:"attribute";s:18:"translatableFields";a:1:{i:0;s:15:"translatedField";}}';

$cm->setCustomRepositoryClassName('DocumentRepository');
$cm->setVersioned(true);
$cm->setUniqueNodeType(true);
$cm->addLifecycleCallback('callback', 'postLoad');
$cm->isMappedSuperclass = true;
$this->assertEquals($expected, serialize($cm));
}

public function testUnserialize()
{
$cm = unserialize('O:40:"Doctrine\ODM\PHPCR\Mapping\ClassMetadata":15:{s:8:"nodeType";s:15:"nt:unstructured";s:10:"identifier";s:2:"id";s:4:"name";s:39:"Doctrine\Tests\ODM\PHPCR\Mapping\Person";s:11:"idGenerator";i:1;s:8:"mappings";a:5:{s:2:"id";a:7:{s:9:"fieldName";s:2:"id";s:2:"id";b:1;s:8:"strategy";s:10:"repository";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;s:8:"property";s:2:"id";}s:8:"username";a:5:{s:9:"fieldName";s:8:"username";s:8:"property";s:8:"username";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:7:"created";a:5:{s:9:"fieldName";s:7:"created";s:8:"property";s:7:"created";s:4:"type";s:8:"datetime";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:6:"locale";a:3:{s:9:"fieldName";s:6:"locale";s:4:"type";s:6:"locale";s:8:"property";s:6:"locale";}s:15:"translatedField";a:7:{s:9:"fieldName";s:15:"translatedField";s:4:"type";s:6:"string";s:10:"translated";b:1;s:8:"property";s:15:"translatedField";s:10:"multivalue";b:0;s:5:"assoc";N;s:8:"nullable";b:0;}}s:13:"fieldMappings";a:4:{i:0;s:2:"id";i:1;s:8:"username";i:2;s:7:"created";i:3;s:15:"translatedField";}s:17:"referenceMappings";a:0:{}s:17:"referrersMappings";a:0:{}s:22:"mixedReferrersMappings";a:0:{}s:16:"childrenMappings";a:0:{}s:13:"childMappings";a:0:{}s:25:"customRepositoryClassName";s:51:"Doctrine\Tests\ODM\PHPCR\Mapping\DocumentRepository";s:18:"isMappedSuperclass";b:1;s:11:"versionable";b:1;s:18:"lifecycleCallbacks";a:1:{s:8:"postLoad";a:1:{i:0;s:8:"callback";}}}');
$cm = unserialize('O:40:"Doctrine\ODM\PHPCR\Mapping\ClassMetadata":16:{s:8:"nodeType";s:15:"nt:unstructured";s:10:"identifier";s:2:"id";s:4:"name";s:39:"Doctrine\Tests\ODM\PHPCR\Mapping\Person";s:11:"idGenerator";i:1;s:8:"mappings";a:5:{s:2:"id";a:7:{s:9:"fieldName";s:2:"id";s:2:"id";b:1;s:8:"strategy";s:10:"repository";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;s:8:"property";s:2:"id";}s:8:"username";a:5:{s:9:"fieldName";s:8:"username";s:8:"property";s:8:"username";s:4:"type";s:6:"string";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:7:"created";a:5:{s:9:"fieldName";s:7:"created";s:8:"property";s:7:"created";s:4:"type";s:8:"datetime";s:10:"multivalue";b:0;s:8:"nullable";b:0;}s:6:"locale";a:3:{s:9:"fieldName";s:6:"locale";s:4:"type";s:6:"locale";s:8:"property";s:6:"locale";}s:15:"translatedField";a:7:{s:9:"fieldName";s:15:"translatedField";s:4:"type";s:6:"string";s:10:"translated";b:1;s:8:"property";s:15:"translatedField";s:10:"multivalue";b:0;s:5:"assoc";N;s:8:"nullable";b:0;}}s:13:"fieldMappings";a:4:{i:0;s:2:"id";i:1;s:8:"username";i:2;s:7:"created";i:3;s:15:"translatedField";}s:17:"referenceMappings";a:0:{}s:17:"referrersMappings";a:0:{}s:22:"mixedReferrersMappings";a:0:{}s:16:"childrenMappings";a:0:{}s:13:"childMappings";a:0:{}s:25:"customRepositoryClassName";s:51:"Doctrine\Tests\ODM\PHPCR\Mapping\DocumentRepository";s:18:"isMappedSuperclass";b:1;s:11:"versionable";b:1;s:14:"uniqueNodeType";b:1;s:18:"lifecycleCallbacks";a:1:{s:8:"postLoad";a:1:{i:0;s:8:"callback";}}}');

$this->assertInstanceOf('Doctrine\ODM\PHPCR\Mapping\ClassMetadata', $cm);

$this->assertEquals(array('callback'), $cm->getLifecycleCallbacks('postLoad'));
$this->assertTrue($cm->isMappedSuperclass);
$this->assertTrue($cm->versionable);
$this->assertTrue($cm->uniqueNodeType);
$this->assertTrue($cm->inheritMixins);
$this->assertEquals('Doctrine\Tests\ODM\PHPCR\Mapping\DocumentRepository', $cm->customRepositoryClassName);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Doctrine\Tests\ODM\PHPCR\Mapping\Model;

use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;

/**
* A class that has a unique node type among other mapped documents.
*
* @PHPCRODM\Document(uniqueNodeType=true)
*/
class UniqueNodeTypeMappingObject
{
/** @PHPCRODM\Id */
public $id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping
https://github.com/doctrine/phpcr-odm/raw/master/doctrine-phpcr-odm-mapping.xsd">
<document name="Doctrine\Tests\ODM\PHPCR\Mapping\Model\UniqueNodeTypeMappingObject" uniqueNodeType="true">
<id name="id" />
</document>
</doctrine-mapping>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Doctrine\Tests\ODM\PHPCR\Mapping\Model\UniqueNodeTypeMappingObject:
uniqueNodeType: true
id: id

0 comments on commit 0b64134

Please sign in to comment.