Skip to content

Commit

Permalink
Make users to add mapping to tree closure tables
Browse files Browse the repository at this point in the history
  • Loading branch information
franmomu committed Dec 27, 2021
1 parent 0a97af5 commit d52d962
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 25 deletions.
24 changes: 22 additions & 2 deletions doc/tree.md
Expand Up @@ -1190,11 +1190,11 @@ You must pass a value in seconds to this parameter.
## Closure Table

To be able to use this strategy, you'll need an additional entity which represents the closures. We already provide you an abstract
entity, so you only need to extend it.
entity, so you need to extend from it and add mapping information for ancestor and descendant.

### Closure Entity

``` php
```php
<?php

namespace YourNamespace\Entity;
Expand All @@ -1204,9 +1204,29 @@ use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\UniqueConstraint(name="closure_unique_idx", columns={"ancestor", "descendant"})
* @ORM\Index(name="closure_depth_idx", columns={"depth"})
*/
#[ORM\Entity]
#[ORM\UniqueConstraint(name: 'closure_unique_idx', columns: ['ancestor', 'descendant'])]
#[ORM\Index(name: 'closure_depth_idx', columns: ['depth'])]
class CategoryClosure extends AbstractClosure
{
/**
* @ORM\ManyToOne(targetEntity="YourNamespace\Entity\Category")
* @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Category::class)]
#[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $ancestor;

/**
* @ORM\ManyToOne(targetEntity="YourNamespace\Entity\Category")
* @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Category::class)]
#[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $descendant;
}
```

Expand Down
110 changes: 92 additions & 18 deletions src/Tree/Strategy/ORM/Closure.php
Expand Up @@ -13,6 +13,7 @@
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
use Gedmo\Exception\RuntimeException;
use Gedmo\Mapping\Event\AdapterInterface;
Expand Down Expand Up @@ -83,11 +84,22 @@ public function getName()
*/
public function processMetadataLoad($em, $meta)
{
// TODO: Remove the body of this method in the next major version.
$config = $this->listener->getConfiguration($em, $meta->getName());
$closureMetadata = $em->getClassMetadata($config['closure']);
$cmf = $em->getMetadataFactory();

$hasTheUserExplicitlyDefinedMapping = true;

if (!$closureMetadata->hasAssociation('ancestor')) {
@trigger_error(sprintf(
'Not adding mapping explicitly to "ancestor" property in "%s" is deprecated and will not work in'
.' version 4.0. You MUST explicitly set the mapping as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table',
$closureMetadata->getName()
), E_USER_DEPRECATED);

$hasTheUserExplicitlyDefinedMapping = false;

// create ancestor mapping
$ancestorMapping = [
'fieldName' => 'ancestor',
Expand Down Expand Up @@ -116,6 +128,14 @@ public function processMetadataLoad($em, $meta)
}

if (!$closureMetadata->hasAssociation('descendant')) {
@trigger_error(sprintf(
'Not adding mapping explicitly to "descendant" property in "%s" is deprecated and will not work in'
.' version 4.0. You MUST explicitly set the mapping as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table',
$closureMetadata->getName()
), E_USER_DEPRECATED);

$hasTheUserExplicitlyDefinedMapping = false;

// create descendant mapping
$descendantMapping = [
'fieldName' => 'descendant',
Expand All @@ -142,24 +162,48 @@ public function processMetadataLoad($em, $meta)
->getAccessibleProperty($closureMetadata->getName(), 'descendant')
;
}
// create unique index on ancestor and descendant
$indexName = substr(strtoupper('IDX_'.md5($closureMetadata->getName())), 0, 20);
$closureMetadata->table['uniqueConstraints'][$indexName] = [
'columns' => [
$this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor')),
$this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant')),
],
];
// this one may not be very useful
$indexName = substr(strtoupper('IDX_'.md5($meta->getName().'depth')), 0, 20);
$closureMetadata->table['indexes'][$indexName] = [
'columns' => ['depth'],
];

$cacheDriver = $cmf->getCacheDriver();

if ($cacheDriver instanceof Cache) {
$cacheDriver->save($closureMetadata->getName().'$CLASSMETADATA', $closureMetadata);

if (!$this->hasClosureTableUniqueConstraint($closureMetadata)) {
@trigger_error(sprintf(
'Not adding a unique constraint explicitly to "%s" is deprecated and will not be automatically'
.' added in version 4.0. You SHOULD explicitly add the unique constraint as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table',
$closureMetadata->getName()
), E_USER_DEPRECATED);

$hasTheUserExplicitlyDefinedMapping = false;

// create unique index on ancestor and descendant
$indexName = substr(strtoupper('IDX_'.md5($closureMetadata->getName())), 0, 20);
$closureMetadata->table['uniqueConstraints'][$indexName] = [
'columns' => [
$this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor')),
$this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant')),
],
];
}

if (!$this->hasClosureTableDepthIndex($closureMetadata)) {
@trigger_error(sprintf(
'Not adding a index with "depth" column explicitly to "%s" is deprecated and will not be automatically'
.' added in version 4.0. You SHOULD explicitly add the index as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table',
$closureMetadata->getName()
), E_USER_DEPRECATED);

$hasTheUserExplicitlyDefinedMapping = false;

// this one may not be very useful
$indexName = substr(strtoupper('IDX_'.md5($meta->getName().'depth')), 0, 20);
$closureMetadata->table['indexes'][$indexName] = [
'columns' => ['depth'],
];
}

if (!$hasTheUserExplicitlyDefinedMapping) {
$cacheDriver = $cmf->getCacheDriver();

if ($cacheDriver instanceof Cache) {
$cacheDriver->save($closureMetadata->getName().'$CLASSMETADATA', $closureMetadata);
}
}
}

Expand Down Expand Up @@ -467,4 +511,34 @@ protected function setLevelFieldOnPendingNodes(ObjectManager $em)
$this->pendingNodesLevelProcess = [];
}
}

private function hasClosureTableUniqueConstraint(ClassMetadata $closureMetadata): bool
{
if (!isset($closureMetadata->table['uniqueConstraints'])) {
return false;
}

foreach ($closureMetadata->table['uniqueConstraints'] as $uniqueConstraint) {
if ([] === array_diff(['ancestor', 'descendant'], $uniqueConstraint['columns'])) {
return true;
}
}

return false;
}

private function hasClosureTableDepthIndex(ClassMetadata $closureMetadata): bool
{
if (!isset($closureMetadata->table['indexes'])) {
return false;
}

foreach ($closureMetadata->table['indexes'] as $uniqueConstraint) {
if ([] === array_diff(['depth'], $uniqueConstraint['columns'])) {
return true;
}
}

return false;
}
}
Expand Up @@ -10,7 +10,7 @@ Gedmo\Tests\Mapping\Fixture\Yaml\ClosureCategory:
gedmo:
tree:
type: closure
closure: Gedmo\Tests\Tree\Fixture\Closure\CategoryClosure
closure: Gedmo\Tests\Tree\Fixture\Closure\CategoryClosureWithoutMapping
fields:
title:
type: string
Expand Down
25 changes: 25 additions & 0 deletions tests/Gedmo/Mapping/Fixture/ClosureTreeClosure.php
Expand Up @@ -12,11 +12,36 @@
namespace Gedmo\Tests\Mapping\Fixture;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Tests\Mapping\Fixture\Xml\ClosureTree;
use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure;

/**
* @ORM\Entity
* @ORM\Table(
* indexes={@ORM\Index(name="closure_tree_depth_idx", columns={"depth"})},
* uniqueConstraints={@ORM\UniqueConstraint(name="closure_tree_unique_idx", columns={
* "ancestor", "descendant"
* })}
* )
*/
#[ORM\Entity]
#[ORM\UniqueConstraint(name: 'closure_tree_unique_idx', columns: ['ancestor', 'descendant'])]
#[ORM\Index(name: 'closure_tree_depth_idx', columns: ['depth'])]
class ClosureTreeClosure extends AbstractClosure
{
/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Mapping\Fixture\Xml\ClosureTree")
* @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: ClosureTree::class)]
#[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $ancestor;

/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Mapping\Fixture\Xml\ClosureTree")
* @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: ClosureTree::class)]
#[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $descendant;
}
8 changes: 4 additions & 4 deletions tests/Gedmo/Mapping/TreeMappingTest.php
Expand Up @@ -17,7 +17,7 @@
use Gedmo\Tests\Mapping\Fixture\Yaml\Category;
use Gedmo\Tests\Mapping\Fixture\Yaml\ClosureCategory;
use Gedmo\Tests\Mapping\Fixture\Yaml\MaterializedPathCategory;
use Gedmo\Tests\Tree\Fixture\Closure\CategoryClosure;
use Gedmo\Tests\Tree\Fixture\Closure\CategoryClosureWithoutMapping;
use Gedmo\Tree\TreeListener;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

Expand Down Expand Up @@ -78,10 +78,10 @@ protected function setUp(): void
public function testApcCached(): void
{
$this->em->getClassMetadata(self::YAML_CLOSURE_CATEGORY);
$this->em->getClassMetadata(CategoryClosure::class);
$this->em->getClassMetadata(CategoryClosureWithoutMapping::class);

$meta = $this->em->getMetadataFactory()->getCacheDriver()->fetch(
'Gedmo\\Tests\\Tree\\Fixture\\Closure\\CategoryClosure$CLASSMETADATA'
'Gedmo\\Tests\\Tree\\Fixture\\Closure\\CategoryClosureWithoutMapping$CLASSMETADATA'
);
static::assertTrue($meta->hasAssociation('ancestor'));
static::assertTrue($meta->hasAssociation('descendant'));
Expand Down Expand Up @@ -120,7 +120,7 @@ public function testYamlClosureMapping(): void
static::assertArrayHasKey('strategy', $config);
static::assertSame('closure', $config['strategy']);
static::assertArrayHasKey('closure', $config);
static::assertSame(CategoryClosure::class, $config['closure']);
static::assertSame(CategoryClosureWithoutMapping::class, $config['closure']);
}

public function testYamlMaterializedPathMapping(): void
Expand Down
23 changes: 23 additions & 0 deletions tests/Gedmo/Tree/Fixture/Closure/CategoryClosure.php
Expand Up @@ -16,8 +16,31 @@

/**
* @ORM\Entity
* @ORM\Table(
* indexes={@ORM\Index(name="closure_category_depth_idx", columns={"depth"})},
* uniqueConstraints={@ORM\UniqueConstraint(name="closure_category_unique_idx", columns={
* "ancestor", "descendant"
* })}
* )
*/
#[ORM\Entity]
#[ORM\UniqueConstraint(name: 'closure_category_unique_idx', columns: ['ancestor', 'descendant'])]
#[ORM\Index(name: 'closure_category_depth_idx', columns: ['depth'])]
class CategoryClosure extends AbstractClosure
{
/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Category")
* @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Category::class)]
#[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $ancestor;

/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Category")
* @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Category::class)]
#[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $descendant;
}
23 changes: 23 additions & 0 deletions tests/Gedmo/Tree/Fixture/Closure/CategoryClosureWithoutMapping.php
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gedmo\Tests\Tree\Fixture\Closure;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure;

/**
* @ORM\Entity
*/
#[ORM\Entity]
class CategoryClosureWithoutMapping extends AbstractClosure
{
}
23 changes: 23 additions & 0 deletions tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevelClosure.php
Expand Up @@ -16,8 +16,31 @@

/**
* @ORM\Entity
* @ORM\Table(
* indexes={@ORM\Index(name="closure_category_without_level_depth_idx", columns={"depth"})},
* uniqueConstraints={@ORM\UniqueConstraint(name="closure_category_without_level_unique_idx", columns={
* "ancestor", "descendant"
* })}
* )
*/
#[ORM\Entity]
#[ORM\UniqueConstraint(name: 'closure_category_without_level_unique_idx', columns: ['ancestor', 'descendant'])]
#[ORM\Index(name: 'closure_category_without_level_depth_idx', columns: ['depth'])]
class CategoryWithoutLevelClosure extends AbstractClosure
{
/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevel")
* @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: CategoryWithoutLevel::class)]
#[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $ancestor;

/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevel")
* @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: CategoryWithoutLevel::class)]
#[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $descendant;
}
23 changes: 23 additions & 0 deletions tests/Gedmo/Tree/Fixture/Closure/PersonClosure.php
Expand Up @@ -16,8 +16,31 @@

/**
* @ORM\Entity
* @ORM\Table(
* indexes={@ORM\Index(name="closure_person_depth_idx", columns={"depth"})},
* uniqueConstraints={@ORM\UniqueConstraint(name="closure_person_unique_idx", columns={
* "ancestor", "descendant"
* })}
* )
*/
#[ORM\Entity]
#[ORM\UniqueConstraint(name: 'closure_person_unique_idx', columns: ['ancestor', 'descendant'])]
#[ORM\Index(name: 'closure_person_depth_idx', columns: ['depth'])]
class PersonClosure extends AbstractClosure
{
/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Person")
* @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Person::class)]
#[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $ancestor;

/**
* @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Person")
* @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Person::class)]
#[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
protected $descendant;
}

0 comments on commit d52d962

Please sign in to comment.