Skip to content

Commit 7390b49

Browse files
committed
Support child restriction and leaf status
1 parent ba31531 commit 7390b49

25 files changed

+643
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
dev-master
5+
----------
6+
7+
* **2016-06-08** [Feature] Allow children of a document to be restricted to a
8+
certain class or forbidden.
9+
410
1.3.1
511
-----
612

doctrine-phpcr-odm-mapping.xsd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
<xs:anyAttribute namespace="##other"/>
4545
</xs:complexType>
4646

47+
<xs:complexType name="child-class">
48+
<xs:attribute name="name" type="xs:string"/>
49+
</xs:complexType>
50+
4751
<xs:complexType name="mixin">
4852
<xs:attribute name="type" type="xs:NMTOKEN"/>
4953
</xs:complexType>
@@ -138,6 +142,7 @@
138142
<xs:element name="document-listeners" type="phpcr:document-listeners" minOccurs="0" maxOccurs="1" />
139143
-->
140144
<xs:element name="id" type="phpcr:id" minOccurs="0" maxOccurs="1" />
145+
<xs:element name="child-class" type="phpcr:child-class" minOccurs="0" maxOccurs="unbounded" />
141146
<xs:element name="uuid" type="phpcr:uuid" minOccurs="0" maxOccurs="1" />
142147
<xs:element name="locale" type="phpcr:locale" minOccurs="0" maxOccurs="1" />
143148
<xs:element name="field" type="phpcr:field" minOccurs="0" maxOccurs="unbounded"/>
@@ -159,6 +164,7 @@
159164
<xs:attribute name="translator" type="xs:NMTOKEN"/>
160165
<xs:attribute name="versionable" type="phpcr:versionable-type" />
161166
<xs:attribute name="referenceable" type="xs:boolean" default="false" />
167+
<xs:attribute name="is-leaf" type="xs:boolean" default="false" />
162168
<xs:attribute name="node-type" type="xs:NMTOKEN" />
163169
<xs:attribute name="repository-class" type="xs:string"/>
164170
<xs:anyAttribute namespace="##other"/>

lib/Doctrine/ODM/PHPCR/Mapping/Annotations/Document.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,14 @@ class Document
6666
* @var boolean
6767
*/
6868
public $uniqueNodeType;
69+
70+
/**
71+
* @var array
72+
*/
73+
public $childClasses = array();
74+
75+
/**
76+
* @var boolean
77+
*/
78+
public $isLeaf;
6979
}

lib/Doctrine/ODM/PHPCR/Mapping/ClassMetadata.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Doctrine\Common\ClassLoader;
3232
use Doctrine\Instantiator\Instantiator;
3333
use Doctrine\Instantiator\InstantiatorInterface;
34+
use Doctrine\ODM\PHPCR\Exception\OutOfBoundsException;
3435

3536
/**
3637
* Metadata class
@@ -328,6 +329,23 @@ class ClassMetadata implements ClassMetadataInterface
328329
*/
329330
public $parentClasses = array();
330331

332+
/**
333+
* READ-ONLY: Child class restrictions.
334+
*
335+
* If empty then any classes are permitted.
336+
*
337+
* @var array
338+
*/
339+
public $childClasses = array();
340+
341+
/**
342+
* READ-ONLY: If the document should be act as a leaf-node and therefore
343+
* not be allowed children.
344+
*
345+
* @var boolean
346+
*/
347+
public $isLeaf = false;
348+
331349
/**
332350
* The inherited fields of this class
333351
*
@@ -453,6 +471,57 @@ public function validateIdentifier()
453471
}
454472
}
455473

474+
/**
475+
* Validate that childClasses is empty if isLeaf is true.
476+
*
477+
* @throws MappingException if there is a conflict between isLeaf and childClasses.
478+
*/
479+
public function validateChildClasses()
480+
{
481+
if (count($this->childClasses) > 0 && $this->isLeaf) {
482+
throw new MappingException(sprintf(
483+
'Cannot map a document as a leaf and define child classes for "%s"',
484+
$this->name
485+
));
486+
}
487+
}
488+
489+
/**
490+
* Assert that the given class FQN can be a child of the document this
491+
* metadata represents.
492+
*
493+
* @param string $classFqn
494+
* @throws OutOfBoundsException
495+
*/
496+
public function assertValidChildClass(ClassMetadata $class)
497+
{
498+
if ($this->isLeaf()) {
499+
throw new OutOfBoundsException(sprintf(
500+
'Document "%s" has been mapped as a leaf. It cannot have children',
501+
$this->name
502+
));
503+
}
504+
505+
$childClasses = $this->getChildClasses();
506+
507+
if (array() === $childClasses) {
508+
return;
509+
}
510+
511+
foreach ($childClasses as $childClass) {
512+
if ($class->name === $childClass || $class->reflClass->isSubclassOf($childClass)) {
513+
return true;
514+
}
515+
}
516+
517+
throw new OutOfBoundsException(sprintf(
518+
'Document "%s" does not allow children of type "%s". Allowed child classes "%s"',
519+
$this->name,
520+
$class->name,
521+
implode('", "', $childClasses)
522+
));
523+
}
524+
456525
/**
457526
* Validate whether this class needs to be referenceable.
458527
*
@@ -1123,6 +1192,49 @@ public function getParentClasses()
11231192
return $this->parentClasses;
11241193
}
11251194

1195+
/**
1196+
* Return the class names or interfaces that children of this document must
1197+
* be an instance of.
1198+
*
1199+
* @return string[]
1200+
*/
1201+
public function getChildClasses()
1202+
{
1203+
return $this->childClasses;
1204+
}
1205+
1206+
/**
1207+
* Set the class names or interfaces that children of this document must be
1208+
* instance of.
1209+
*
1210+
* @param string[] $childClasses
1211+
*/
1212+
public function setChildClasses(array $childClasses)
1213+
{
1214+
$this->childClasses = $childClasses;
1215+
}
1216+
1217+
/**
1218+
* Return true if this is designated as a leaf node.
1219+
*
1220+
* @return bool
1221+
*/
1222+
public function isLeaf()
1223+
{
1224+
return $this->isLeaf;
1225+
}
1226+
1227+
/**
1228+
* Set if this document should act as a leaf node.
1229+
*
1230+
* @param bool $isLeaf
1231+
*/
1232+
public function setIsLeaf($isLeaf)
1233+
{
1234+
$this->isLeaf = $isLeaf;
1235+
}
1236+
1237+
11261238
/**
11271239
* Checks whether the class will generate an id via the repository.
11281240
*

lib/Doctrine/ODM/PHPCR/Mapping/ClassMetadataFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ protected function validateRuntimeMetadata($class, $parent)
256256
$class->validateIdentifier();
257257
$class->validateReferenceable();
258258
$class->validateReferences();
259+
$class->validateChildClasses();
259260
$class->validateLifecycleCallbacks($this->getReflectionService());
260261
$class->validateTranslatables();
261262

lib/Doctrine/ODM/PHPCR/Mapping/Driver/AnnotationDriver.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
111111
$metadata->setTranslator($documentAnnot->translator);
112112
}
113113

114+
if (array() !== $documentAnnot->childClasses) {
115+
$metadata->setChildClasses($documentAnnot->childClasses);
116+
}
117+
118+
$metadata->setIsLeaf($documentAnnot->isLeaf);
119+
114120
foreach ($reflClass->getProperties() as $property) {
115121
if ($metadata->isInheritedField($property->name)
116122
&& $metadata->name !== $property->getDeclaringClass()->getName()

lib/Doctrine/ODM/PHPCR/Mapping/Driver/XmlDriver.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ public function loadMetadataForClass($className, ClassMetadata $class)
8383
$class->setUniqueNodeType((bool) $xmlRoot['uniqueNodeType']);
8484
}
8585

86+
if (isset($xmlRoot['is-leaf'])) {
87+
if (!in_array($value = $xmlRoot['is-leaf'], array('true', 'false'))) {
88+
throw new MappingException(sprintf(
89+
'Value of is-leaf must be "true" or "false", got "%s" for class "%s"',
90+
$value, $className
91+
));
92+
}
93+
94+
$class->setIsLeaf($value == 'true' ? true : false);
95+
}
96+
97+
8698
if (isset($xmlRoot->mixins)) {
8799
$mixins = array();
88100
foreach ($xmlRoot->mixins->mixin as $mixin) {
@@ -259,6 +271,14 @@ public function loadMetadataForClass($className, ClassMetadata $class)
259271
$class->mapField($mapping);
260272
}
261273

274+
if (isset($xmlRoot->{'child-class'})) {
275+
$childClasses = array();
276+
foreach ($xmlRoot->{'child-class'} as $requiredClass) {
277+
$childClasses[] = $requiredClass['name'];
278+
}
279+
$class->setChildClasses($childClasses);
280+
}
281+
262282
$class->validateClassMapping();
263283
}
264284

lib/Doctrine/ODM/PHPCR/Mapping/Driver/YamlDriver.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ public function loadMetadataForClass($className, ClassMetadata $class)
255255
}
256256
}
257257

258+
if (isset($element['child_classes'])) {
259+
$class->setChildClasses($element['child_classes']);
260+
}
261+
262+
if (isset($element['is_leaf'])) {
263+
$class->setIsLeaf($element['is_leaf']);
264+
}
265+
258266
$class->validateClassMapping();
259267
}
260268

lib/Doctrine/ODM/PHPCR/UnitOfWork.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
use PHPCR\Util\PathHelper;
5555
use PHPCR\Util\NodeHelper;
5656
use Jackalope\Session as JackalopeSession;
57+
use Doctrine\ODM\PHPCR\Exception\OutOfBoundsException;
5758

5859
/**
5960
* Unit of work class
@@ -2332,6 +2333,8 @@ private function executeInserts($documents)
23322333
}
23332334

23342335
$parentNode = $this->session->getNode(PathHelper::getParentPath($id));
2336+
$this->validateChildClass($parentNode, $class);
2337+
23352338
$nodename = PathHelper::getNodeName($id);
23362339
$node = $parentNode->addNode($nodename, $class->nodeType);
23372340
if ($class->node) {
@@ -2731,6 +2734,9 @@ private function executeMoves($documents)
27312734
);
27322735
}
27332736

2737+
$parentNode = $this->session->getNode(PathHelper::getParentPath($targetPath));
2738+
$this->validateChildClass($parentNode, $class);
2739+
27342740
$this->session->move($sourcePath, $targetPath);
27352741

27362742
// update fields nodename, parentMapping and depth if they exist in this type
@@ -3968,4 +3974,23 @@ public function invokeGlobalEvent($eventName, EventArgs $event)
39683974
$this->eventManager->dispatchEvent($eventName, $event);
39693975
}
39703976
}
3977+
3978+
/**
3979+
* If the parent node has child restrictions, ensure that the given
3980+
* class name is within them.
3981+
*
3982+
* @param NodeInterface $parentNode
3983+
* @param string $classFqn
3984+
*/
3985+
private function validateChildClass(NodeInterface $parentNode, ClassMetadata $class)
3986+
{
3987+
$parentClass = $this->documentClassMapper->getClassName($this->dm, $parentNode);
3988+
3989+
if (null === $parentClass) {
3990+
return;
3991+
}
3992+
3993+
$metadata = $this->dm->getClassMetadata($parentClass);
3994+
$metadata->assertValidChildClass($class);
3995+
}
39713996
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Doctrine\Tests\Models\CMS;
4+
5+
use Doctrine\Common\Collections\ArrayCollection;
6+
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;
7+
8+
/**
9+
* @PHPCRODM\Document(childClasses={"Doctrine\Tests\Models\CMS\CmsArticle"})
10+
*/
11+
class CmsArticleFolder
12+
{
13+
/**
14+
* @PHPCRODM\Id
15+
*/
16+
public $id;
17+
18+
/**
19+
* @PHPCRODM\Children()
20+
*/
21+
public $articles;
22+
}

0 commit comments

Comments
 (0)