Skip to content

Commit 7c85797

Browse files
committed
Add closure tree operations in 2.4.x
1 parent 5ee093a commit 7c85797

File tree

2 files changed

+206
-1
lines changed

2 files changed

+206
-1
lines changed

lib/Gedmo/Tree/Entity/Repository/ClosureTreeRepository.php

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,209 @@ protected function validate()
397397
{
398398
return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::CLOSURE;
399399
}
400+
401+
public function verify()
402+
{
403+
$nodeMeta = $this->getClassMetadata();
404+
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
405+
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
406+
$closureMeta = $this->_em->getClassMetadata($config['closure']);
407+
$errors = [];
408+
409+
$q = $this->_em->createQuery("
410+
SELECT COUNT(node)
411+
FROM {$nodeMeta->name} AS node
412+
LEFT JOIN {$closureMeta->name} AS c WITH c.ancestor = node AND c.depth = 0
413+
WHERE c.id IS NULL
414+
");
415+
416+
if ($missingSelfRefsCount = intval($q->getSingleScalarResult())) {
417+
$errors[] = "Missing $missingSelfRefsCount self referencing closures";
418+
}
419+
420+
$q = $this->_em->createQuery("
421+
SELECT COUNT(node)
422+
FROM {$nodeMeta->name} AS node
423+
INNER JOIN {$closureMeta->name} AS c1 WITH c1.descendant = node.{$config['parent']}
424+
LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor
425+
WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor
426+
");
427+
428+
if ($missingClosuresCount = intval($q->getSingleScalarResult())) {
429+
$errors[] = "Missing $missingClosuresCount closures";
430+
}
431+
432+
$q = $this->_em->createQuery("
433+
SELECT COUNT(c1.id)
434+
FROM {$closureMeta->name} AS c1
435+
LEFT JOIN {$nodeMeta->name} AS node WITH c1.descendant = node.$nodeIdField
436+
LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor
437+
WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor
438+
");
439+
440+
if ($invalidClosuresCount = intval($q->getSingleScalarResult())) {
441+
$errors[] = "Found $invalidClosuresCount invalid closures";
442+
}
443+
444+
if (!empty($config['level'])) {
445+
$levelField = $config['level'];
446+
$maxResults = 1000;
447+
$q = $this->_em->createQuery("
448+
SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level
449+
FROM {$nodeMeta->name} AS node
450+
INNER JOIN {$closureMeta->name} AS c WITH c.descendant = node.$nodeIdField
451+
GROUP BY node.id, node.level
452+
HAVING node_level IS NULL OR node_level <> closure_level
453+
")->setMaxResults($maxResults);
454+
455+
if ($invalidLevelsCount = count($q->getScalarResult())) {
456+
$errors[] = "Found $invalidLevelsCount invalid level values";
457+
}
458+
}
459+
460+
return $errors ?: true;
461+
}
462+
463+
public function recover()
464+
{
465+
if ($this->verify() === true) {
466+
return;
467+
}
468+
469+
$this->cleanUpClosure();
470+
$this->rebuildClosure();
471+
}
472+
473+
public function rebuildClosure()
474+
{
475+
$nodeMeta = $this->getClassMetadata();
476+
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
477+
$closureMeta = $this->_em->getClassMetadata($config['closure']);
478+
479+
$insertClosures = function ($entries) use ($closureMeta) {
480+
$closureTable = $closureMeta->getTableName();
481+
$ancestorColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('ancestor'));
482+
$descendantColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('descendant'));
483+
$depthColumnName = $closureMeta->getColumnName('depth');
484+
485+
$conn = $this->_em->getConnection();
486+
$conn->beginTransaction();
487+
foreach ($entries as $entry) {
488+
$conn->insert($closureTable, array_combine(
489+
[$ancestorColumnName, $descendantColumnName, $depthColumnName],
490+
$entry
491+
));
492+
}
493+
$conn->commit();
494+
};
495+
496+
$buildClosures = function ($dql) use ($insertClosures) {
497+
$newClosuresCount = 0;
498+
$batchSize = 1000;
499+
$q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false);
500+
do {
501+
$entries = $q->getScalarResult();
502+
$insertClosures($entries);
503+
$newClosuresCount += count($entries);
504+
} while (count($entries) > 0);
505+
return $newClosuresCount;
506+
};
507+
508+
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
509+
$newClosuresCount = $buildClosures("
510+
SELECT node.id AS ancestor, node.$nodeIdField AS descendant, 0 AS depth
511+
FROM {$nodeMeta->name} AS node
512+
LEFT JOIN {$closureMeta->name} AS c WITH c.ancestor = node AND c.depth = 0
513+
WHERE c.id IS NULL
514+
");
515+
$newClosuresCount += $buildClosures("
516+
SELECT IDENTITY(c1.ancestor) AS ancestor, node.$nodeIdField AS descendant, c1.depth + 1 AS depth
517+
FROM {$nodeMeta->name} AS node
518+
INNER JOIN {$closureMeta->name} AS c1 WITH c1.descendant = node.{$config['parent']}
519+
LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor
520+
WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor
521+
");
522+
523+
return $newClosuresCount;
524+
}
525+
526+
public function cleanUpClosure()
527+
{
528+
$conn = $this->_em->getConnection();
529+
$nodeMeta = $this->getClassMetadata();
530+
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
531+
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
532+
$closureMeta = $this->_em->getClassMetadata($config['closure']);
533+
$closureTableName = $closureMeta->getTableName();
534+
535+
$dql = "
536+
SELECT c1.id AS id
537+
FROM {$closureMeta->name} AS c1
538+
LEFT JOIN {$nodeMeta->name} AS node WITH c1.descendant = node.$nodeIdField
539+
LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor
540+
WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor
541+
";
542+
543+
$deletedClosuresCount = 0;
544+
$batchSize = 1000;
545+
$q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false);
546+
547+
while (($ids = $q->getScalarResult()) && !empty($ids)) {
548+
$ids = array_map(function ($el) {
549+
return $el['id'];
550+
}, $ids);
551+
$query = "DELETE FROM {$closureTableName} WHERE id IN (".implode(', ', $ids).")";
552+
if (!$conn->executeQuery($query)) {
553+
throw new \RuntimeException('Failed to remove incorrect closures');
554+
}
555+
$deletedClosuresCount += count($ids);
556+
}
557+
558+
return $deletedClosuresCount;
559+
}
560+
561+
public function updateLevelValues()
562+
{
563+
$nodeMeta = $this->getClassMetadata();
564+
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
565+
$levelUpdatesCount = 0;
566+
567+
if (!empty($config['level'])) {
568+
$levelField = $config['level'];
569+
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
570+
$closureMeta = $this->_em->getClassMetadata($config['closure']);
571+
572+
$batchSize = 1000;
573+
$q = $this->_em->createQuery("
574+
SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level
575+
FROM {$nodeMeta->name} AS node
576+
INNER JOIN {$closureMeta->name} AS c WITH c.descendant = node.$nodeIdField
577+
GROUP BY node.id, node.level
578+
HAVING node_level IS NULL OR node_level <> closure_level
579+
")->setMaxResults($batchSize)->setCacheable(false);
580+
do {
581+
$entries = $q->getScalarResult();
582+
$this->_em->getConnection()->beginTransaction();
583+
foreach ($entries as $entry) {
584+
unset($entry['node_level']);
585+
$this->_em->createQuery("
586+
UPDATE {$nodeMeta->name} AS node SET node.$levelField = :closure_level WHERE node.$nodeIdField = :id
587+
")->execute($entry);
588+
}
589+
$this->_em->getConnection()->commit();
590+
$levelUpdatesCount += count($entries);
591+
} while (count($entries) > 0);
592+
}
593+
594+
return $levelUpdatesCount;
595+
}
596+
597+
protected function getJoinColumnFieldName($association)
598+
{
599+
if (count($association['joinColumnFieldNames']) > 1) {
600+
throw new \RuntimeException('More association on field ' . $association['fieldName']);
601+
}
602+
603+
return array_shift($association['joinColumnFieldNames']);
604+
}
400605
}

lib/Gedmo/Tree/Strategy/ORM/Closure.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ public function updateNode(EntityManager $em, $node, $oldParent)
443443
}
444444
// using subquery directly, sqlite acts unfriendly
445445
$query = "DELETE FROM {$table} WHERE id IN (".implode(', ', $ids).")";
446-
if (!$conn->executeQuery($query)) {
446+
if (!empty($ids) && !$conn->executeQuery($query)) {
447447
throw new RuntimeException('Failed to remove old closures');
448448
}
449449
}

0 commit comments

Comments
 (0)