Passed
Push — master ( 891151...7b6b24 )
by Julito
12:45
created

ClosureTreeRepositoryTrait::ancestorsCount()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 15
nc 6
nop 2
dl 0
loc 29
rs 9.4555
c 1
b 0
f 0
1
<?php
2
3
namespace Chamilo\CoreBundle\Traits\Repository\ORM;
4
5
use Doctrine\ORM\QueryBuilder;
6
use Gedmo\Exception\InvalidArgumentException;
7
use Doctrine\ORM\Query;
8
use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure;
9
use Gedmo\Tree\Entity\Repository\ClosureTreeRepository;
10
use Gedmo\Tree\Strategy;
11
use Gedmo\Tool\Wrapper\EntityWrapper;
12
13
/**
14
 * The ClosureTreeRepository has some useful functions
15
 * to interact with Closure tree. Repository uses
16
 * the strategy used by listener
17
 *
18
 * @author Gustavo Adrian <[email protected]>
19
 * @author Gediminas Morkevicius <[email protected]>
20
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
21
 */
22
trait ClosureTreeRepositoryTrait
23
{
24
    use TreeRepositoryTrait;
25
26
    /**
27
     * {@inheritDoc}
28
     */
29
    public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
30
    {
31
        $meta = $this->getClassMetadata();
32
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
33
34
        $qb = $this->getQueryBuilder();
35
        $qb->select('node')
36
            ->from($config['useObjectClass'], 'node')
37
            ->where('node.'.$config['parent']." IS NULL");
38
39
        if ($sortByField) {
40
            $qb->orderBy('node.'.$sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc');
41
        }
42
43
        return $qb;
44
    }
45
46
    /**
47
     * {@inheritDoc}
48
     */
49
    public function getRootNodesQuery($sortByField = null, $direction = 'asc')
50
    {
51
        return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
52
    }
53
54
    /**
55
     * {@inheritDoc}
56
     */
57
    public function getRootNodes($sortByField = null, $direction = 'asc')
58
    {
59
        return $this->getRootNodesQuery($sortByField, $direction)->getResult();
60
    }
61
62
    /**
63
     * Get the Tree path query by given $node
64
     *
65
     * @param object $node
66
     *
67
     * @throws InvalidArgumentException - if input is not valid
68
     *
69
     * @return Query
70
     */
71
    public function getPathQuery($node)
72
    {
73
        $meta = $this->getClassMetadata();
74
        $em = $this->getEntityManager();
75
76
        if (!$node instanceof $meta->name) {
77
            throw new InvalidArgumentException("Node is not related to this repository");
78
        }
79
80
        if (!$em->getUnitOfWork()->isInIdentityMap($node)) {
81
            throw new InvalidArgumentException("Node is not managed by UnitOfWork");
82
        }
83
84
        $config = $this->listener->getConfiguration($em, $meta->name);
85
        $closureMeta = $em->getClassMetadata($config['closure']);
86
87
        $dql = "SELECT c, node FROM {$closureMeta->name} c";
88
        $dql .= " INNER JOIN c.ancestor node";
89
        $dql .= " WHERE c.descendant = :node";
90
        $dql .= " ORDER BY c.depth DESC";
91
        $q = $em->createQuery($dql);
92
        $q->setParameters(compact('node'));
93
94
        return $q;
95
    }
96
97
    /**
98
     * Get the Tree path of Nodes by given $node
99
     *
100
     * @param object $node
101
     *
102
     * @return array - list of Nodes in path
103
     */
104
    public function getPath($node)
105
    {
106
        return array_map(function (AbstractClosure $closure) {
107
            return $closure->getAncestor();
108
        }, $this->getPathQuery($node)->getResult());
109
    }
110
111
    /**
112
     * Get list of nodes related to a given $node
113
     * @param string  $way         - search direction: "down" (for children) or "up" (for ancestors)
114
     * @param object  $node        - if null, all tree nodes will be taken
115
     * @param boolean $direct      - true to take only direct children or parents
116
     * @param string  $sortByField - field name to sort by
117
     * @param string  $direction   - sort direction : "ASC" or "DESC"
118
     * @param bool    $includeNode - Include the root node in results?
119
     *
120
     * @return array - list of given $node parents, null on failure
121
     */
122
    public function closureLocateQueryBuilder($way = 'down', $node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
123
    {
124
        switch($way) {
125
            case 'down':
126
                $first = 'ancestor';
127
                $second = 'descendant';
128
                break;
129
            case 'up':
130
                $first = 'descendant';
131
                $second = 'ancestor';
132
                break;
133
            default:
134
                throw new InvalidArgumentException("Direction must be 'up' or 'down' but '$way' found");
135
        }
136
137
        $meta = $this->getClassMetadata();
138
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
139
        $qb = $this->getQueryBuilder();
140
141
        if ($node !== null) {
142
            if ($node instanceof $meta->name) {
143
                if (!$this->getEntityManager()->getUnitOfWork()->isInIdentityMap($node)) {
144
                    throw new InvalidArgumentException('Node is not managed by UnitOfWork');
145
                }
146
                $where = "c.$first = :node AND ";
147
                $qb->select('c, node')
148
                    ->from($config['closure'], 'c')
149
                    ->innerJoin("c.$second", 'node');
150
                if ($direct) {
151
                    $where .= 'c.depth = 1';
152
                } else {
153
                    $where .= "c.$second <> :node";
154
                }
155
                $qb->where($where);
156
                if ($includeNode) {
157
                    $qb->orWhere("c.$first = :node AND c.$second = :node");
158
                }
159
            } else {
160
                throw new \InvalidArgumentException("Node is not related to this repository");
161
            }
162
        } else {
163
            $qb->select('node')
164
                ->from($config['useObjectClass'], 'node');
165
            if ($direct) {
166
                $qb->where('node.'.$config['parent'].' IS NULL');
167
            }
168
        }
169
170
        if ($sortByField) {
171
            if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
172
                $qb->orderBy('node.'.$sortByField, $direction);
173
            } else {
174
                throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
175
            }
176
        }
177
178
        if ($node) {
179
            $qb->setParameter('node', $node);
180
        }
181
182
        return $qb;
183
    }
184
185
    /**
186
     * @see getChildrenQueryBuilder
187
     */
188
    public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
189
    {
190
        return $this->closureLocateQueryBuilder('down', $node, $direct, $sortByField, $direction, $includeNode);
191
    }
192
193
    /**
194
     * @see getChildrenQuery
195
     */
196
    public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
197
    {
198
        return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
199
    }
200
201
    /**
202
     * @see getChildren
203
     */
204
    public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
205
    {
206
        $result = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult();
207
208
        if ($node) {
209
            $result = array_map(function (AbstractClosure $closure) {
210
                return $closure->getDescendant();
211
            }, $result);
212
        }
213
214
        return $result;
215
    }
216
217
    /**
218
     * {@inheritDoc}
219
     */
220
    public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
221
    {
222
        return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode);
223
    }
224
225
    /**
226
     * {@inheritDoc}
227
     */
228
    public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
229
    {
230
        return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode);
231
    }
232
233
    /**
234
     * {@inheritDoc}
235
     */
236
    public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
237
    {
238
        return $this->children($node, $direct, $sortByField, $direction, $includeNode);
239
    }
240
241
    /**
242
     * @see getAncestorsQueryBuilder
243
     */
244
    public function ancestorsQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
245
    {
246
        return $this->closureLocateQueryBuilder('up', $node, $direct, $sortByField, $direction, $includeNode);
247
    }
248
249
    /**
250
     * @see getAncestorsQuery
251
     */
252
    public function ancestorsQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
253
    {
254
        return $this->ancestorsQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
255
    }
256
257
    /**
258
     * @see getAncestors
259
     */
260
    public function ancestors($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
261
    {
262
        $result = $this->ancestorsQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult();
263
264
        if ($node) {
265
            $result = array_map(function (AbstractClosure $closure) {
266
                return $closure->getAncestor();
267
            }, $result);
268
        }
269
270
        return $result;
271
    }
272
273
    /**
274
     * Get the list of ancestors that lead to the given $node. This returns a QueryBuilder object
275
     *
276
     * @param object  $node        - if null, all tree nodes will be taken
277
     * @param boolean $direct      - true to take only direct children
278
     * @param string  $sortByField - field name to sort by
279
     * @param string  $direction   - sort direction : "ASC" or "DESC"
280
     * @param bool    $includeNode - Include the root node in results?
281
     *
282
     * @return QueryBuilder - QueryBuilder object
283
     */
284
    public function getAncestorsQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
285
    {
286
        return $this->ancestorsQueryBuilder($node, $direct, $sortByField, $direction, $includeNode);
287
    }
288
289
    /**
290
     * Get the list of ancestors that lead to the given $node. This returns a Query object
291
     *
292
     * @param object  $node        - if null, all tree nodes will be taken
293
     * @param boolean $direct      - true to take only direct children
294
     * @param string  $sortByField - field name to sort by
295
     * @param string  $direction   - sort direction : "ASC" or "DESC"
296
     * @param bool    $includeNode - Include the root node in results?
297
     *
298
     * @return Query - Query object
299
     */
300
    public function getAncestorsQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
301
    {
302
        return $this->ancestorsQuery($node, $direct, $sortByField, $direction, $includeNode);
303
    }
304
305
    /**
306
     * Get the list of ancestors that lead to the given $node
307
     *
308
     * @param object  $node        - if null, all tree nodes will be taken
309
     * @param boolean $direct      - true to take only direct children
310
     * @param string  $sortByField - field name to sort by
311
     * @param string  $direction   - sort direction : "ASC" or "DESC"
312
     * @param bool    $includeNode - Include the root node in results?
313
     *
314
     * @return array - list of given $node parents, null on failure
315
     */
316
    public function getAncestors($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
317
    {
318
        return $this->ancestors($node, $direct, $sortByField, $direction, $includeNode);
319
    }
320
321
    /**
322
     * @see childrenCount
323
     */
324
    public function ancestorsCount($node = null, $direct = false)
325
    {
326
        $meta = $this->getClassMetadata();
327
328
        if (is_object($node)) {
329
            if (!($node instanceof $meta->name)) {
330
                throw new InvalidArgumentException("Node is not related to this repository");
331
            }
332
            $wrapped = new EntityWrapper($node, $this->getEntityManager());
333
            if (!$wrapped->hasValidIdentifier()) {
334
                throw new InvalidArgumentException("Node is not managed by UnitOfWork");
335
            }
336
        }
337
338
        $qb = $this->getAncestorsQueryBuilder($node, $direct);
339
        // We need to remove the ORDER BY DQL part since some vendors could throw an error
340
        // in count queries
341
        $dqlParts = $qb->getDQLParts();
342
        // We need to check first if there's an ORDER BY DQL part, because resetDQLPart doesn't
343
        // check if its internal array has an "orderby" index
344
        if (isset($dqlParts['orderBy'])) {
345
            $qb->resetDQLPart('orderBy');
346
        }
347
348
        $aliases = $qb->getRootAliases();
349
        $alias = $aliases[0];
350
        $qb->select('COUNT('.$alias.')');
351
352
        return (int) $qb->getQuery()->getSingleScalarResult();
353
    }
354
355
    /**
356
     * Removes given $node from the tree and reparents its descendants
357
     *
358
     * @todo may be improved, to issue single query on reparenting
359
     *
360
     * @param object $node
361
     *
362
     * @throws \Gedmo\Exception\InvalidArgumentException
363
     * @throws \Gedmo\Exception\RuntimeException         - if something fails in transaction
364
     */
365
    public function removeFromTree($node)
366
    {
367
        $meta = $this->getClassMetadata();
368
        $em = $this->getEntityManager();
369
370
        if (!$node instanceof $meta->name) {
371
            throw new InvalidArgumentException("Node is not related to this repository");
372
        }
373
374
        $wrapped = new EntityWrapper($node, $em);
375
        if (!$wrapped->hasValidIdentifier()) {
376
            throw new InvalidArgumentException("Node is not managed by UnitOfWork");
377
        }
378
379
        $config = $this->listener->getConfiguration($em, $meta->name);
380
        $pk = $meta->getSingleIdentifierFieldName();
381
        $nodeId = $wrapped->getIdentifier();
382
        $parent = $wrapped->getPropertyValue($config['parent']);
383
384
        $dql = "SELECT node FROM {$config['useObjectClass']} node";
385
        $dql .= " WHERE node.{$config['parent']} = :node";
386
        $q = $em->createQuery($dql);
387
        $q->setParameters(compact('node'));
388
        $nodesToReparent = $q->getResult();
389
390
        // process updates in transaction
391
        $em->getConnection()->beginTransaction();
392
        try {
393
            foreach ($nodesToReparent as $nodeToReparent) {
394
                $id = $meta->getReflectionProperty($pk)->getValue($nodeToReparent);
395
                $meta->getReflectionProperty($config['parent'])->setValue($nodeToReparent, $parent);
396
397
                $dql = "UPDATE {$config['useObjectClass']} node";
398
                $dql .= " SET node.{$config['parent']} = :parent";
399
                $dql .= " WHERE node.{$pk} = :id";
400
                $q = $em->createQuery($dql);
401
                $q->setParameters(compact('parent', 'id'));
402
                $q->getSingleScalarResult();
403
404
                $this->listener
405
                    ->getStrategy($em, $meta->name)
406
                    ->updateNode($em, $nodeToReparent, $node);
407
                $oid = spl_object_hash($nodeToReparent);
408
                $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $parent);
409
            }
410
411
            $dql = "DELETE {$config['useObjectClass']} node";
412
            $dql .= " WHERE node.{$pk} = :nodeId";
413
            $q = $em->createQuery($dql);
414
            $q->setParameters(compact('nodeId'));
415
            $q->getSingleScalarResult();
416
            $em->getConnection()->commit();
417
        } catch (\Exception $e) {
418
            $em->close();
419
            $em->getConnection()->rollback();
420
421
            throw new \Gedmo\Exception\RuntimeException('Transaction failed: '.$e->getMessage(), null, $e);
422
        }
423
424
        // remove from identity map
425
        $em->getUnitOfWork()->removeFromIdentityMap($node);
426
        $node = null;
427
    }
428
    /**
429
     * Process nodes and produce an array with the
430
     * structure of the tree
431
     *
432
     * @param array - Array of nodes
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
433
     *
434
     * @return array - Array with tree structure
435
     */
436
    public function buildTreeArray(array $nodes)
437
    {
438
        $meta = $this->getClassMetadata();
439
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
440
        $nestedTree = array();
441
        $idField = $meta->getSingleIdentifierFieldName();
442
        $hasLevelProp = !empty($config['level']);
443
        $levelProp = $hasLevelProp ? $config['level'] : ClosureTreeRepository::SUBQUERY_LEVEL;
444
        $childrenIndex = $this->repoUtils->getChildrenIndex();
445
446
        if (count($nodes) > 0) {
447
            $firstLevel = $hasLevelProp ? $nodes[0][0]['descendant'][$levelProp] : $nodes[0][$levelProp];
448
            $l = 1;     // 1 is only an initial value. We could have a tree which has a root node with any level (subtrees)
449
            $refs = array();
450
            foreach ($nodes as $n) {
451
                $node = $n[0]['descendant'];
452
                $node[$childrenIndex] = array();
453
                $level = $hasLevelProp ? $node[$levelProp] : $n[$levelProp];
454
                if ($l < $level) {
455
                    $l = $level;
456
                }
457
                if ($l == $firstLevel) {
458
                    $tmp = &$nestedTree;
459
                } else {
460
                    $tmp = &$refs[$n['parent_id']][$childrenIndex];
461
                }
462
                $key = count($tmp);
463
                $tmp[$key] = $node;
464
                $refs[$node[$idField]] = &$tmp[$key];
465
            }
466
            unset($refs);
467
        }
468
469
        return $nestedTree;
470
    }
471
472
    /**
473
     * {@inheritdoc}
474
     */
475
    public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false)
476
    {
477
        return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult();
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     */
483
    public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false)
484
    {
485
        return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery();
486
    }
487
488
    /**
489
     * {@inheritdoc}
490
     */
491
    public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false)
492
    {
493
        $meta = $this->getClassMetadata();
494
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
495
        $idField = $meta->getSingleIdentifierFieldName();
496
        $subQuery = '';
497
        $hasLevelProp = isset($config['level']) && $config['level'];
498
499
        if (!$hasLevelProp) {
500
            $subQuery = ', (SELECT MAX(c2.depth) + 1 FROM '.$config['closure'];
501
            $subQuery .= ' c2 WHERE c2.descendant = c.descendant GROUP BY c2.descendant) AS '.ClosureTreeRepository::SUBQUERY_LEVEL;
502
        }
503
504
        $q = $this->getEntityManager()->createQueryBuilder()
505
            ->select('c, node, p.'.$idField.' AS parent_id'.$subQuery)
506
            ->from($config['closure'], 'c')
507
            ->innerJoin('c.descendant', 'node')
508
            ->leftJoin('node.'.$config['parent'], 'p')
509
            ->addOrderBy(($hasLevelProp ? 'node.'.$config['level'] : ClosureTreeRepository::SUBQUERY_LEVEL), 'asc');
510
511
        if ($node !== null) {
512
            $q->where('c.ancestor = :node');
513
            $q->setParameters(compact('node'));
514
        } else {
515
            $q->groupBy('c.descendant');
516
        }
517
518
        if (!$includeNode) {
519
            $q->andWhere('c.ancestor != c.descendant');
520
        }
521
522
        $defaultOptions = array();
523
        $options = array_merge($defaultOptions, $options);
524
        if (isset($options['childSort']) && is_array($options['childSort']) &&
525
            isset($options['childSort']['field']) && isset($options['childSort']['dir'])) {
526
            $q->addOrderBy(
527
                'node.'.$options['childSort']['field'],
528
                strtolower($options['childSort']['dir']) == 'asc' ? 'asc' : 'desc'
529
            );
530
        }
531
532
        return $q;
533
    }
534
535
    public function verify()
536
    {
537
        $nodeMeta = $this->getClassMetadata();
538
        $nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
539
        $config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
540
        $closureMeta = $this->_em->getClassMetadata($config['closure']);
541
        $errors = [];
542
543
        $q = $this->_em->createQuery("
544
          SELECT COUNT(node)
545
          FROM {$nodeMeta->name} AS node
546
          LEFT JOIN {$closureMeta->name} AS c WITH c.ancestor = node AND c.depth = 0
547
          WHERE c.id IS NULL
548
        ");
549
550
        if ($missingSelfRefsCount = intval($q->getSingleScalarResult())) {
551
            $errors[] = "Missing $missingSelfRefsCount self referencing closures";
552
        }
553
554
        $q = $this->_em->createQuery("
555
          SELECT COUNT(node)
556
          FROM {$nodeMeta->name} AS node
557
          INNER JOIN {$closureMeta->name} AS c1 WITH c1.descendant = node.{$config['parent']}
558
          LEFT  JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor
559
          WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor
560
        ");
561
562
        if ($missingClosuresCount = intval($q->getSingleScalarResult())) {
563
            $errors[] = "Missing $missingClosuresCount closures";
564
        }
565
566
        return $errors ?: true;
567
    }
568
569
    public function recover()
570
    {
571
        if ($this->verify() === true) {
572
            return;
573
        }
574
575
        $this->cleanUpClosure();
576
        $this->rebuildClosure();
577
    }
578
579
    public function rebuildClosure()
580
    {
581
        $nodeMeta = $this->getClassMetadata();
582
        $config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
583
        $closureMeta = $this->_em->getClassMetadata($config['closure']);
584
585
        $insertClosures = function ($entries) use ($closureMeta) {
586
            $closureTable = $closureMeta->getTableName();
587
            $ancestorColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('ancestor'));
588
            $descendantColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('descendant'));
589
            $depthColumnName = $closureMeta->getColumnName('depth');
590
591
            $conn = $this->_em->getConnection();
592
            $conn->beginTransaction();
593
            foreach ($entries as $entry) {
594
                $conn->insert($closureTable, array_combine(
595
                    [$ancestorColumnName, $descendantColumnName, $depthColumnName],
596
                    $entry
597
                ));
598
            }
599
            $conn->commit();
600
        };
601
602
        $buildClosures = function ($dql) use ($insertClosures) {
603
            $newClosuresCount = 0;
604
            $batchSize = 1000;
605
            $q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false);
606
            do {
607
                $entries = $q->getScalarResult();
608
                $insertClosures($entries);
609
                $newClosuresCount += count($entries);
610
            } while (count($entries) > 0);
611
            return $newClosuresCount;
612
        };
613
614
        $nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
615
        $newClosuresCount = $buildClosures("
616
          SELECT node.id AS ancestor, node.$nodeIdField AS descendant, 0 AS depth
617
          FROM {$nodeMeta->name} AS node
618
          LEFT JOIN {$closureMeta->name} AS c WITH c.ancestor = node AND c.depth = 0
619
          WHERE c.id IS NULL
620
        ");
621
        $newClosuresCount += $buildClosures("
622
          SELECT IDENTITY(c1.ancestor) AS ancestor, node.$nodeIdField AS descendant, c1.depth + 1 AS depth
623
          FROM {$nodeMeta->name} AS node
624
          INNER JOIN {$closureMeta->name} AS c1 WITH c1.descendant = node.{$config['parent']}
625
          LEFT  JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor
626
          WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor
627
        ");
628
629
        return $newClosuresCount;
630
    }
631
632
    public function cleanUpClosure()
633
    {
634
        $conn = $this->_em->getConnection();
635
        $nodeMeta = $this->getClassMetadata();
636
        $nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
637
        $config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
638
        $closureMeta = $this->_em->getClassMetadata($config['closure']);
639
        $closureTableName = $closureMeta->getTableName();
640
641
        $dql = "
642
            SELECT c1.id AS id
643
            FROM {$closureMeta->name} AS c1
644
            LEFT JOIN {$nodeMeta->name} AS node WITH c1.descendant = node.$nodeIdField
645
            LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor
646
            WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor
647
        ";
648
649
        $deletedClosuresCount = 0;
650
        $batchSize = 1000;
651
        $q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false);
652
653
        while (($ids = $q->getScalarResult()) && !empty($ids)) {
654
            $ids = array_map(function ($el) {
655
                return $el['id'];
656
            }, $ids);
657
            $query = "DELETE FROM {$closureTableName} WHERE id IN (".implode(', ', $ids).")";
658
            if (!$conn->executeQuery($query)) {
659
                throw new \RuntimeException('Failed to remove incorrect closures');
660
            }
661
            $deletedClosuresCount += count($ids);
662
        }
663
664
        return $deletedClosuresCount;
665
    }
666
667
    protected function getJoinColumnFieldName($association)
668
    {
669
        if (count($association['joinColumnFieldNames']) > 1) {
670
            throw new \RuntimeException('More association on field ' . $association['fieldName']);
671
        }
672
673
        return array_shift($association['joinColumnFieldNames']);
674
    }
675
676
    /**
677
     * {@inheritdoc}
678
     */
679
    protected function validate()
680
    {
681
        return $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName() === Strategy::CLOSURE;
682
    }
683
}
684