Skip to content

Commit

Permalink
Merge pull request #667 from sarcher/left-join
Browse files Browse the repository at this point in the history
Specify unique node types via mapping to allow proper outer joins
  • Loading branch information
dbu committed Nov 6, 2015
2 parents 2edc458 + 0b86153 commit ec40d24
Show file tree
Hide file tree
Showing 18 changed files with 467 additions and 7 deletions.
1 change: 1 addition & 0 deletions bin/phpcrodm.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
new \Doctrine\ODM\PHPCR\Tools\Console\Command\GenerateProxiesCommand(),
new \Doctrine\ODM\PHPCR\Tools\Console\Command\DumpQueryBuilderReferenceCommand(),
new \Doctrine\ODM\PHPCR\Tools\Console\Command\InfoDoctrineCommand(),
new \Doctrine\ODM\PHPCR\Tools\Console\Command\VerifyUniqueNodeTypesMappingCommand(),
new \Doctrine\ODM\PHPCR\Tools\Console\Command\RegisterSystemNodeTypesCommand(),
));
if (isset($extraCommands) && ! empty($extraCommands)) {
Expand Down
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
12 changes: 7 additions & 5 deletions lib/Doctrine/ODM/PHPCR/Query/Builder/ConverterPhpcr.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,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 +278,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/

namespace Doctrine\ODM\PHPCR\Tools\Console\Command;

use Doctrine\ODM\PHPCR\Tools\Helper\UniqueNodeTypeHelper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Verify that any documents which are mapped as having unique
* node types are truly unique.
*/
class VerifyUniqueNodeTypesMappingCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
parent::configure();

$this
->setName('doctrine:phpcr:mapping:verify-unique-node-types')
->setDescription('Verify that documents claiming to have unique node types are truly unique')
->setHelp(<<<EOT
The <info>%command.name%</info> command checks all mapped PHPCR-ODM documents
and verifies that any claiming to use unique node types are truly unique.
EOT
);
}

/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$documentManager = $this->getHelper('phpcr')->getDocumentManager();
$uniqueNodeTypeHelper = new UniqueNodeTypeHelper();

$debugInformation = $uniqueNodeTypeHelper->checkNodeTypeMappings($documentManager);

if (OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity()) {
foreach ($debugInformation as $className => $debug) {
$output->writeln(sprintf(
'The document <info>%s</info> uses %snode type <info>%s</info>',
$className,
$debug['unique_node_type'] ? '<comment>uniquely mapped</comment> ' : '',
$debug['node_type']
));
}
}

return 0;
}
}
67 changes: 67 additions & 0 deletions lib/Doctrine/ODM/PHPCR/Tools/Helper/UniqueNodeTypeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/

namespace Doctrine\ODM\PHPCR\Tools\Helper;

use Doctrine\ODM\PHPCR\DocumentManagerInterface;
use Doctrine\ODM\PHPCR\Mapping\MappingException;

/**
* Provides unique node type mapping verification.
*/
class UniqueNodeTypeHelper
{
/**
* Check each mapped PHPCR-ODM document for the given document manager,
* throwing an exception if any document is set to use a unique node
* type but the node type is re-used. Returns an array of debug information.
*
* @param DocumentManagerInterface $documentManager The document manager to check mappings for.
*
* @return array
*
* @throws MappingException
*/
public function checkNodeTypeMappings(DocumentManagerInterface $documentManager)
{
$knownNodeTypes = array();
$debugInformation = array();
$allMetadata = $documentManager->getMetadataFactory()->getAllMetadata();

foreach ($allMetadata as $classMetadata) {
if ($classMetadata->hasUniqueNodeType() && isset($knownNodeTypes[$classMetadata->getNodeType()])) {
throw new MappingException(sprintf(
'The class "%s" is mapped with uniqueNodeType set to true, but the node type "%s" is used by "%s" as well.',
$classMetadata->name,
$classMetadata->getNodeType(),
$knownNodeTypes[$classMetadata->getNodeType()]
));
}

$knownNodeTypes[$classMetadata->getNodeType()] = $classMetadata->name;

$debugInformation[$classMetadata->name] = array(
'unique_node_type' => $classMetadata->hasUniqueNodeType(),
'node_type' => $classMetadata->getNodeType()
);
}

return $debugInformation;
}
}
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
Loading

0 comments on commit ec40d24

Please sign in to comment.