Completed
Push — ezp_31107 ( 816f32...9177e0 )
by
unknown
220:54 queued 209:22
created

DoctrineDatabase::getSubtreeNodesData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 22
rs 9.568
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
8
9
use Doctrine\DBAL\ParameterType;
10
use Doctrine\DBAL\Connection;
11
use Doctrine\DBAL\FetchMode;
12
use Doctrine\DBAL\Query\QueryBuilder;
13
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
14
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator;
15
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
16
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
17
use eZ\Publish\SPI\Persistence\Content\Location;
18
use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
19
use eZ\Publish\SPI\Persistence\Content\Location\CreateStruct;
20
use eZ\Publish\API\Repository\Values\Content\Query;
21
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
22
use RuntimeException;
23
use PDO;
24
use function time;
25
26
/**
27
 * Location gateway implementation using the Doctrine database.
28
 *
29
 * @internal Gateway implementation is considered internal. Use Persistence Location Handler instead.
30
 *
31
 * @see \eZ\Publish\SPI\Persistence\Content\Location\Handler
32
 */
33
final class DoctrineDatabase extends Gateway
34
{
35
    private const SORT_CLAUSE_TARGET_MAP = [
36
        'location_depth' => 'depth',
37
        'location_priority' => 'priority',
38
        'location_path' => 'path_string',
39
    ];
40
41
    /** @var \Doctrine\DBAL\Connection */
42
    private $connection;
43
44
    /** @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator */
45
    private $languageMaskGenerator;
46
47
    /** @var \Doctrine\DBAL\Platforms\AbstractPlatform */
48
    private $dbPlatform;
49
50
    /**
51
     * @throws \Doctrine\DBAL\DBALException
52
     */
53
    public function __construct(Connection $connection, MaskGenerator $languageMaskGenerator)
54
    {
55
        $this->connection = $connection;
56
        $this->dbPlatform = $this->connection->getDatabasePlatform();
57
        $this->languageMaskGenerator = $languageMaskGenerator;
58
    }
59
60
    public function getBasicNodeData(
61
        int $nodeId,
62
        array $translations = null,
63
        bool $useAlwaysAvailable = true
64
    ): array {
65
        $query = $this->createNodeQueryBuilder(['t.*'], $translations, $useAlwaysAvailable);
66
        $query->andWhere(
67
            $query->expr()->eq('t.node_id', $query->createNamedParameter($nodeId, ParameterType::INTEGER))
68
        );
69
70
        if ($row = $query->execute()->fetch(FetchMode::ASSOCIATIVE)) {
71
            return $row;
72
        }
73
74
        throw new NotFound('location', $nodeId);
75
    }
76
77
    public function getNodeDataList(array $locationIds, array $translations = null, bool $useAlwaysAvailable = true): iterable
78
    {
79
        $query = $this->createNodeQueryBuilder(['t.*'], $translations, $useAlwaysAvailable);
80
        $query->andWhere(
81
            $query->expr()->in(
82
                't.node_id',
83
                $query->createNamedParameter($locationIds, Connection::PARAM_INT_ARRAY)
84
            )
85
        );
86
87
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
88
    }
89
90
    public function getBasicNodeDataByRemoteId(
91
        string $remoteId,
92
        array $translations = null,
93
        bool $useAlwaysAvailable = true
94
    ): array {
95
        $query = $this->createNodeQueryBuilder(['t.*'], $translations, $useAlwaysAvailable);
96
        $query->andWhere(
97
            $query->expr()->eq('t.remote_id', $query->createNamedParameter($remoteId, ParameterType::STRING))
98
        );
99
100
        if ($row = $query->execute()->fetch(FetchMode::ASSOCIATIVE)) {
101
            return $row;
102
        }
103
104
        throw new NotFound('location', $remoteId);
105
    }
106
107
    public function loadLocationDataByContent(int $contentId, ?int $rootLocationId = null): array
108
    {
109
        $query = $this->connection->createQueryBuilder();
110
        $query
111
            ->select('*')
112
            ->from(self::CONTENT_TREE_TABLE, 't')
113
            ->where(
114
                $query->expr()->eq(
115
                    't.contentobject_id',
116
                    $query->createPositionalParameter($contentId, ParameterType::INTEGER)
117
                )
118
            );
119
120
        if ($rootLocationId !== null) {
121
            $query
122
                ->andWhere(
123
                    $this->getSubtreeLimitationExpression($query, $rootLocationId)
124
                )
125
            ;
126
        }
127
128
        $statement = $query->execute();
129
130
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
131
    }
132
133
    public function loadParentLocationsDataForDraftContent(int $contentId): array
134
    {
135
        $query = $this->connection->createQueryBuilder();
136
        $expr = $query->expr();
137
        $query
138
            ->select('DISTINCT t.*')
139
            ->from(self::CONTENT_TREE_TABLE, 't')
140
            ->innerJoin(
141
                't',
142
                'eznode_assignment',
143
                'a',
144
                $expr->andX(
145
                    $expr->eq(
146
                        't.node_id',
147
                        'a.parent_node'
148
                    ),
149
                    $expr->eq(
150
                        'a.contentobject_id',
151
                        $query->createPositionalParameter($contentId, ParameterType::INTEGER)
152
                    ),
153
                    $expr->eq(
154
                        'a.op_code',
155
                        $query->createPositionalParameter(
156
                            self::NODE_ASSIGNMENT_OP_CODE_CREATE,
157
                            ParameterType::INTEGER
158
                        )
159
                    )
160
                )
161
            )
162
            ->innerJoin(
163
                'a',
164
                'ezcontentobject',
165
                'c',
166
                $expr->andX(
167
                    $expr->eq(
168
                        'a.contentobject_id',
169
                        'c.id'
170
                    ),
171
                    $expr->eq(
172
                        'c.status',
173
                        $query->createPositionalParameter(
174
                            ContentInfo::STATUS_DRAFT,
175
                            ParameterType::INTEGER
176
                        )
177
                    )
178
                )
179
            );
180
181
        $statement = $query->execute();
182
183
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
184
    }
185
186
    public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array
187
    {
188
        $query = $this->connection->createQueryBuilder();
189
        $query
190
            ->select($onlyIds ? 'node_id, contentobject_id, depth' : '*')
191
            ->from(self::CONTENT_TREE_TABLE, 't')
192
            ->where($this->getSubtreeLimitationExpression($query, $sourceId))
193
            ->orderBy('t.depth')
194
            ->addOrderBy('t.node_id');
195
        $statement = $query->execute();
196
197
        $results = $statement->fetchAll($onlyIds ? (FetchMode::COLUMN | PDO::FETCH_GROUP) : FetchMode::ASSOCIATIVE);
198
        // array_map() is used to to map all elements stored as $results[$i][0] to $results[$i]
199
        return $onlyIds ? array_map('reset', $results) : $results;
200
    }
201
202
    /**
203
     * Return constraint which limits the given $query to the subtree starting at $rootLocationId.
204
     */
205
    private function getSubtreeLimitationExpression(
206
        QueryBuilder $query,
207
        int $rootLocationId
208
    ): string {
209
        return $query->expr()->like(
210
            't.path_string',
211
            $query->createPositionalParameter(
212
                '%/' . ((string)$rootLocationId) . '/%',
213
                ParameterType::STRING
214
            )
215
        );
216
    }
217
218
    public function getChildren(int $locationId): array
219
    {
220
        $query = $this->connection->createQueryBuilder();
221
        $query->select('*')->from(
222
            self::CONTENT_TREE_TABLE
223
        )->where(
224
            $query->expr()->eq(
225
                'ezcontentobject_tree.parent_node_id',
226
                $query->createPositionalParameter($locationId, ParameterType::INTEGER)
227
            )
228
        );
229
        $statement = $query->execute();
230
231
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
232
    }
233
234
    private function getSubtreeNodesData(string $pathString): array
235
    {
236
        $query = $this->connection->createQueryBuilder();
237
        $query
238
            ->select(
239
                'node_id',
240
                'parent_node_id',
241
                'path_string',
242
                'path_identification_string',
243
                'is_hidden'
244
            )
245
            ->from(self::CONTENT_TREE_TABLE)
246
            ->where(
247
                $query->expr()->like(
248
                    'path_string',
249
                    $query->createPositionalParameter($pathString . '%', ParameterType::STRING)
250
                )
251
            );
252
        $statement = $query->execute();
253
254
        return $statement->fetchAll();
255
    }
256
257
    public function moveSubtreeNodes(array $sourceNodeData, array $destinationNodeData): void
258
    {
259
        $fromPathString = $sourceNodeData['path_string'];
260
261
        $rows = $this->getSubtreeNodesData($fromPathString);
262
263
        $oldParentPathString = implode('/', array_slice(explode('/', $fromPathString), 0, -2)) . '/';
264
        $oldParentPathIdentificationString = implode(
265
            '/',
266
            array_slice(explode('/', $sourceNodeData['path_identification_string']), 0, -1)
267
        );
268
269
        $hiddenNodeIds = $this->getHiddenNodeIds($rows);
270
        foreach ($rows as $row) {
271
            // Prefixing ensures correct replacement when old parent is root node
272
            $newPathString = str_replace(
273
                'prefix' . $oldParentPathString,
274
                $destinationNodeData['path_string'],
275
                'prefix' . $row['path_string']
276
            );
277
            $replace = rtrim($destinationNodeData['path_identification_string'], '/');
278
            if (empty($oldParentPathIdentificationString)) {
279
                $replace .= '/';
280
            }
281
            $newPathIdentificationString = str_replace(
282
                'prefix' . $oldParentPathIdentificationString,
283
                $replace,
284
                'prefix' . $row['path_identification_string']
285
            );
286
            $newParentId = $row['parent_node_id'];
287
            if ($row['path_string'] === $fromPathString) {
288
                $newParentId = (int)implode('', array_slice(explode('/', $newPathString), -3, 1));
289
            }
290
291
            $this->moveSingleSubtreeNode(
292
                (int)$row['node_id'],
293
                $sourceNodeData,
294
                $destinationNodeData,
295
                $newPathString,
296
                $newPathIdentificationString,
297
                $newParentId,
298
                $hiddenNodeIds
299
            );
300
        }
301
    }
302
303
    private function getHiddenNodeIds(array $rows): array
304
    {
305
        return array_map(
306
            static function (array $row) {
307
                return (int)$row['node_id'];
308
            },
309
            array_filter(
310
                $rows,
311
                static function (array $row) {
312
                    return !empty($row['is_hidden']);
313
                }
314
            )
315
        );
316
    }
317
318
    /**
319
     * @param int[] $hiddenNodeIds
320
     */
321
    private function isHiddenByParent(string $pathString, array $hiddenNodeIds): bool
322
    {
323
        $parentNodeIds = array_map('intval', explode('/', trim($pathString, '/')));
324
        array_pop($parentNodeIds); // remove self
325
        foreach ($parentNodeIds as $parentNodeId) {
326
            if (in_array($parentNodeId, $hiddenNodeIds, true)) {
327
                return true;
328
            }
329
        }
330
331
        return false;
332
    }
333
334
    /**
335
     * @param array $sourceNodeData
336
     * @param array $destinationNodeData
337
     * @param int[] $hiddenNodeIds
338
     */
339
    private function moveSingleSubtreeNode(
340
        int $nodeId,
341
        array $sourceNodeData,
342
        array $destinationNodeData,
343
        string $newPathString,
344
        string $newPathIdentificationString,
345
        int $newParentId,
346
        array $hiddenNodeIds
347
    ): void {
348
        $query = $this->connection->createQueryBuilder();
349
        $query
350
            ->update(self::CONTENT_TREE_TABLE)
351
            ->set(
352
                'path_string',
353
                $query->createPositionalParameter($newPathString, ParameterType::STRING)
354
            )
355
            ->set(
356
                'path_identification_string',
357
                $query->createPositionalParameter(
358
                    $newPathIdentificationString,
359
                    ParameterType::STRING
360
                )
361
            )
362
            ->set(
363
                'depth',
364
                $query->createPositionalParameter(
365
                    substr_count($newPathString, '/') - 2,
366
                    ParameterType::INTEGER
367
                )
368
            )
369
            ->set(
370
                'parent_node_id',
371
                $query->createPositionalParameter($newParentId, ParameterType::INTEGER)
372
            );
373
374
        if ($destinationNodeData['is_hidden'] || $destinationNodeData['is_invisible']) {
375
            // CASE 1: Mark whole tree as invisible if destination is invisible and/or hidden
376
            $query->set(
377
                'is_invisible',
378
                $query->createPositionalParameter(1, ParameterType::INTEGER)
379
            );
380
        } elseif (!$sourceNodeData['is_hidden'] && $sourceNodeData['is_invisible']) {
381
            // CASE 2: source is only invisible, we will need to re-calculate whole moved tree visibility
382
            $query->set(
383
                'is_invisible',
384
                $query->createPositionalParameter(
385
                    $this->isHiddenByParent($newPathString, $hiddenNodeIds) ? 1 : 0,
386
                    ParameterType::INTEGER
387
                )
388
            );
389
        }
390
391
        $query->where(
392
            $query->expr()->eq(
393
                'node_id',
394
                $query->createPositionalParameter($nodeId, ParameterType::INTEGER)
395
            )
396
        );
397
        $query->execute();
398
    }
399
400
    public function updateSubtreeModificationTime(string $pathString, ?int $timestamp = null): void
401
    {
402
        $nodes = array_filter(explode('/', $pathString));
403
        $query = $this->connection->createQueryBuilder();
404
        $query
405
            ->update(self::CONTENT_TREE_TABLE)
406
            ->set(
407
                'modified_subnode',
408
                $query->createPositionalParameter(
409
                    $timestamp ?: time(), ParameterType::INTEGER
410
                )
411
            )
412
            ->where(
413
                $query->expr()->in(
414
                    'node_id',
415
                    $nodes
416
                )
417
            );
418
        $query->execute();
419
    }
420
421
    public function hideSubtree(string $pathString): void
422
    {
423
        $this->setNodeWithChildrenInvisible($pathString);
424
        $this->setNodeHidden($pathString);
425
    }
426
427
    public function setNodeWithChildrenInvisible(string $pathString): void
428
    {
429
        $query = $this->connection->createQueryBuilder();
430
        $query
431
            ->update(self::CONTENT_TREE_TABLE)
432
            ->set(
433
                'is_invisible',
434
                $query->createPositionalParameter(1, ParameterType::INTEGER)
435
            )
436
            ->set(
437
                'modified_subnode',
438
                $query->createPositionalParameter(time(), ParameterType::INTEGER)
439
            )
440
            ->where(
441
                $query->expr()->like(
442
                    'path_string',
443
                    $query->createPositionalParameter($pathString . '%', ParameterType::STRING)
444
                )
445
            );
446
447
        $query->execute();
448
    }
449
450
    public function setNodeHidden(string $pathString): void
451
    {
452
        $this->setNodeHiddenStatus($pathString, true);
453
    }
454
455
    private function setNodeHiddenStatus(string $pathString, bool $isHidden): void
456
    {
457
        $query = $this->connection->createQueryBuilder();
458
        $query
459
            ->update(self::CONTENT_TREE_TABLE)
460
            ->set(
461
                'is_hidden',
462
                $query->createPositionalParameter((int) $isHidden, ParameterType::INTEGER)
463
            )
464
            ->where(
465
                $query->expr()->eq(
466
                    'path_string',
467
                    $query->createPositionalParameter($pathString, ParameterType::STRING)
468
                )
469
            );
470
471
        $query->execute();
472
    }
473
474
    public function unHideSubtree(string $pathString): void
475
    {
476
        $this->setNodeUnhidden($pathString);
477
        $this->setNodeWithChildrenVisible($pathString);
478
    }
479
480
    public function setNodeWithChildrenVisible(string $pathString): void
481
    {
482
        // Check if any parent nodes are explicitly hidden
483
        if ($this->isAnyNodeInPathExplicitlyHidden($pathString)) {
484
            // There are parent nodes set hidden, so that we can skip marking
485
            // something visible again.
486
            return;
487
        }
488
489
        // Find nodes of explicitly hidden subtrees in the subtree which
490
        // should remain unhidden
491
        $hiddenSubtrees = $this->loadHiddenSubtreesByPath($pathString);
492
493
        $query = $this->connection->createQueryBuilder();
494
        $expr = $query->expr();
495
        $query
496
            ->update(self::CONTENT_TREE_TABLE)
497
            ->set(
498
                'is_invisible',
499
                $query->createPositionalParameter(0, ParameterType::INTEGER)
500
            )
501
            ->set(
502
                'modified_subnode',
503
                $query->createPositionalParameter(time(), ParameterType::INTEGER)
504
            );
505
506
        // Build where expression selecting the nodes, which should not be made hidden
507
        $query
508
            ->where(
509
                $expr->like(
510
                    'path_string',
511
                    $query->createPositionalParameter($pathString . '%', ParameterType::STRING)
512
                )
513
            );
514
        if (count($hiddenSubtrees) > 0) {
515
            foreach ($hiddenSubtrees as $subtreePathString) {
516
                $query
517
                    ->andWhere(
518
                        $expr->notLike(
519
                            'path_string',
520
                            $query->createPositionalParameter(
521
                                $subtreePathString . '%',
522
                                ParameterType::STRING
523
                            )
524
                        )
525
                    );
526
            }
527
        }
528
529
        $query->execute();
530
    }
531
532
    private function isAnyNodeInPathExplicitlyHidden(string $pathString): bool
533
    {
534
        $query = $this->buildHiddenSubtreeQuery(
535
            $this->dbPlatform->getCountExpression('path_string')
536
        );
537
        $expr = $query->expr();
538
        $query
539
            ->andWhere(
540
                $expr->in(
541
                    't.node_id',
542
                    $query->createPositionalParameter(
543
                        array_filter(explode('/', $pathString)),
544
                        Connection::PARAM_INT_ARRAY
545
                    )
546
                )
547
            );
548
        $count = (int)$query->execute()->fetchColumn();
549
550
        return $count > 0;
551
    }
552
553
    /**
554
     * @return array list of path strings
555
     */
556
    private function loadHiddenSubtreesByPath(string $pathString): array
557
    {
558
        $query = $this->buildHiddenSubtreeQuery('path_string');
559
        $expr = $query->expr();
560
        $query
561
            ->andWhere(
562
                $expr->like(
563
                    'path_string',
564
                    $query->createPositionalParameter(
565
                        $pathString . '%',
566
                        ParameterType::STRING
567
                    )
568
                )
569
            );
570
        $statement = $query->execute();
571
572
        return $statement->fetchAll(FetchMode::COLUMN);
573
    }
574
575
    private function buildHiddenSubtreeQuery(string $selectExpr): QueryBuilder
576
    {
577
        $query = $this->connection->createQueryBuilder();
578
        $expr = $query->expr();
579
        $query
580
            ->select($selectExpr)
581
            ->from(self::CONTENT_TREE_TABLE, 't')
582
            ->leftJoin('t', 'ezcontentobject', 'c', 't.contentobject_id = c.id')
583
            ->where(
584
                $expr->orX(
585
                    $expr->eq(
586
                        't.is_hidden',
587
                        $query->createPositionalParameter(1, ParameterType::INTEGER)
588
                    ),
589
                    $expr->eq(
590
                        'c.is_hidden',
591
                        $query->createPositionalParameter(1, ParameterType::INTEGER)
592
                    )
593
                )
594
            );
595
596
        return $query;
597
    }
598
599
    public function setNodeUnhidden(string $pathString): void
600
    {
601
        $this->setNodeHiddenStatus($pathString, false);
602
    }
603
604
    public function swap(int $locationId1, int $locationId2): bool
605
    {
606
        $queryBuilder = $this->connection->createQueryBuilder();
607
        $expr = $queryBuilder->expr();
608
        $queryBuilder
609
            ->select('node_id', 'main_node_id', 'contentobject_id', 'contentobject_version')
610
            ->from(self::CONTENT_TREE_TABLE)
611
            ->where(
612
                $expr->in(
613
                    'node_id',
614
                    ':locationIds'
615
                )
616
            )
617
            ->setParameter('locationIds', [$locationId1, $locationId2], Connection::PARAM_INT_ARRAY)
618
        ;
619
        $statement = $queryBuilder->execute();
620
        $contentObjects = [];
621
        foreach ($statement->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
622
            $row['is_main_node'] = (int)$row['main_node_id'] === (int)$row['node_id'];
623
            $contentObjects[$row['node_id']] = $row;
624
        }
625
626
        if (!isset($contentObjects[$locationId1], $contentObjects[$locationId2])) {
627
            throw new RuntimeException(
628
                sprintf(
629
                    '%s: failed to fetch either Location %d or Location %d',
630
                    __METHOD__,
631
                    $locationId1,
632
                    $locationId2
633
                )
634
            );
635
        }
636
        $content1data = $contentObjects[$locationId1];
637
        $content2data = $contentObjects[$locationId2];
638
639
        $queryBuilder = $this->connection->createQueryBuilder();
640
        $queryBuilder
641
            ->update(self::CONTENT_TREE_TABLE)
642
            ->set('contentobject_id', ':contentId')
643
            ->set('contentobject_version', ':versionNo')
644
            ->set('main_node_id', ':mainNodeId')
645
            ->where(
646
                $expr->eq('node_id', ':locationId')
647
            );
648
649
        $queryBuilder
650
            ->setParameter(':contentId', $content2data['contentobject_id'])
651
            ->setParameter(':versionNo', $content2data['contentobject_version'])
652
            ->setParameter(
653
                ':mainNodeId',
654
                // make main Location main again, preserve main Location id of non-main one
655
                $content2data['is_main_node']
656
                    ? $content1data['node_id']
657
                    : $content2data['main_node_id']
658
            )
659
            ->setParameter('locationId', $locationId1);
660
661
        // update Location 1 entry
662
        $queryBuilder->execute();
663
664
        $queryBuilder
665
            ->setParameter(':contentId', $content1data['contentobject_id'])
666
            ->setParameter(':versionNo', $content1data['contentobject_version'])
667
            ->setParameter(
668
                ':mainNodeId',
669
                $content1data['is_main_node']
670
                    // make main Location main again, preserve main Location id of non-main one
671
                    ? $content2data['node_id']
672
                    : $content1data['main_node_id']
673
            )
674
            ->setParameter('locationId', $locationId2);
675
676
        // update Location 2 entry
677
        $queryBuilder->execute();
678
679
        return true;
680
    }
681
682
    public function create(CreateStruct $createStruct, array $parentNode): Location
683
    {
684
        $location = $this->insertLocationIntoContentTree($createStruct, $parentNode);
685
686
        $mainLocationId = $createStruct->mainLocationId === true ? $location->id : $createStruct->mainLocationId;
687
        $location->pathString = $parentNode['path_string'] . $location->id . '/';
688
        $query = $this->connection->createQueryBuilder();
689
        $query
690
            ->update(self::CONTENT_TREE_TABLE)
691
            ->set(
692
                'path_string',
693
                $query->createPositionalParameter($location->pathString, ParameterType::STRING)
694
            )
695
            ->set(
696
                'main_node_id',
697
                $query->createPositionalParameter($mainLocationId, ParameterType::INTEGER)
698
            )
699
            ->where(
700
                $query->expr()->eq(
701
                    'node_id',
702
                    $query->createPositionalParameter($location->id, ParameterType::INTEGER)
703
                )
704
            );
705
706
        $query->execute();
707
708
        return $location;
709
    }
710
711
    public function createNodeAssignment(
712
        CreateStruct $createStruct,
713
        int $parentNodeId,
714
        int $type = self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP
715
    ): void {
716
        $isMain = ($createStruct->mainLocationId === true ? 1 : 0);
717
718
        $query = $this->connection->createQueryBuilder();
719
        $query
720
            ->insert('eznode_assignment')
721
            ->values(
722
                [
723
                    'contentobject_id' => ':contentobject_id',
724
                    'contentobject_version' => ':contentobject_version',
725
                    'from_node_id' => ':from_node_id',
726
                    'is_main' => ':is_main',
727
                    'op_code' => ':op_code',
728
                    'parent_node' => ':parent_node',
729
                    'parent_remote_id' => ':parent_remote_id',
730
                    'remote_id' => ':remote_id',
731
                    'sort_field' => ':sort_field',
732
                    'sort_order' => ':sort_order',
733
                    'priority' => ':priority',
734
                    'is_hidden' => ':is_hidden',
735
                ]
736
            )
737
            ->setParameters(
738
                [
739
                    'contentobject_id' => $createStruct->contentId,
740
                    'contentobject_version' => $createStruct->contentVersion,
741
                    // from_node_id: unused field
742
                    'from_node_id' => 0,
743
                    // is_main: changed by the business layer, later
744
                    'is_main' => $isMain,
745
                    'op_code' => $type,
746
                    'parent_node' => $parentNodeId,
747
                    // parent_remote_id column should contain the remote id of the corresponding Location
748
                    'parent_remote_id' => $createStruct->remoteId,
749
                    // remote_id column should contain the remote id of the node assignment itself,
750
                    // however this was never implemented completely in Legacy Stack, so we just set
751
                    // it to default value '0'
752
                    'remote_id' => '0',
753
                    'sort_field' => $createStruct->sortField,
754
                    'sort_order' => $createStruct->sortOrder,
755
                    'priority' => $createStruct->priority,
756
                    'is_hidden' => $createStruct->hidden,
757
                ],
758
                [
759
                    'contentobject_id' => ParameterType::INTEGER,
760
                    'contentobject_version' => ParameterType::INTEGER,
761
                    'from_node_id' => ParameterType::INTEGER,
762
                    'is_main' => ParameterType::INTEGER,
763
                    'op_code' => ParameterType::INTEGER,
764
                    'parent_node' => ParameterType::INTEGER,
765
                    'parent_remote_id' => ParameterType::STRING,
766
                    'remote_id' => ParameterType::STRING,
767
                    'sort_field' => ParameterType::INTEGER,
768
                    'sort_order' => ParameterType::INTEGER,
769
                    'priority' => ParameterType::INTEGER,
770
                    'is_hidden' => ParameterType::INTEGER,
771
                ]
772
            );
773
        $query->execute();
774
    }
775
776
    public function deleteNodeAssignment(int $contentId, ?int $versionNo = null): void
777
    {
778
        $query = $this->connection->createQueryBuilder();
779
        $query->delete(
780
            'eznode_assignment'
781
        )->where(
782
            $query->expr()->eq(
783
                'contentobject_id',
784
                $query->createPositionalParameter($contentId, ParameterType::INTEGER)
785
            )
786
        );
787
        if (isset($versionNo)) {
788
            $query->andWhere(
789
                $query->expr()->eq(
790
                    'contentobject_version',
791
                    $query->createPositionalParameter($versionNo, ParameterType::INTEGER)
792
                )
793
            );
794
        }
795
        $query->execute();
796
    }
797
798
    public function updateNodeAssignment(
799
        int $contentObjectId,
800
        int $oldParent,
801
        int $newParent,
802
        int $opcode
803
    ): void {
804
        $query = $this->connection->createQueryBuilder();
805
        $query
806
            ->update('eznode_assignment')
807
            ->set(
808
                'parent_node',
809
                $query->createPositionalParameter($newParent, ParameterType::INTEGER)
810
            )
811
            ->set(
812
                'op_code',
813
                $query->createPositionalParameter($opcode, ParameterType::INTEGER)
814
            )
815
            ->where(
816
                $query->expr()->eq(
817
                    'contentobject_id',
818
                    $query->createPositionalParameter(
819
                        $contentObjectId,
820
                        ParameterType::INTEGER
821
                    )
822
                )
823
            )
824
            ->andWhere(
825
                $query->expr()->eq(
826
                    'parent_node',
827
                    $query->createPositionalParameter(
828
                        $oldParent,
829
                        ParameterType::INTEGER
830
                    )
831
                )
832
            );
833
        $query->execute();
834
    }
835
836
    public function createLocationsFromNodeAssignments(int $contentId, int $versionNo): void
837
    {
838
        // select all node assignments with OP_CODE_CREATE (3) for this content
839
        $query = $this->connection->createQueryBuilder();
840
        $query
841
            ->select('*')
842
            ->from('eznode_assignment')
843
            ->where(
844
                $query->expr()->eq(
845
                    'contentobject_id',
846
                    $query->createPositionalParameter($contentId, ParameterType::INTEGER)
847
                )
848
            )
849
            ->andWhere(
850
                $query->expr()->eq(
851
                    'contentobject_version',
852
                    $query->createPositionalParameter($versionNo, ParameterType::INTEGER)
853
                )
854
            )
855
            ->andWhere(
856
                $query->expr()->eq(
857
                    'op_code',
858
                    $query->createPositionalParameter(
859
                        self::NODE_ASSIGNMENT_OP_CODE_CREATE,
860
                        ParameterType::INTEGER
861
                    )
862
                )
863
            )
864
            ->orderBy('id');
865
        $statement = $query->execute();
866
867
        // convert all these assignments to nodes
868
869
        while ($row = $statement->fetch(FetchMode::ASSOCIATIVE)) {
870
            $isMain = (bool)$row['is_main'];
871
            // set null for main to indicate that new Location ID is required
872
            $mainLocationId = $isMain ? null : $this->getMainNodeId($contentId);
873
874
            $parentLocationData = $this->getBasicNodeData((int)$row['parent_node']);
875
            $isInvisible = $row['is_hidden'] || $parentLocationData['is_hidden'] || $parentLocationData['is_invisible'];
876
            $this->create(
877
                new CreateStruct(
878
                    [
879
                        'contentId' => $row['contentobject_id'],
880
                        'contentVersion' => $row['contentobject_version'],
881
                        // BC layer: for CreateStruct "true" means that a main Location should be created
882
                        'mainLocationId' => $mainLocationId ?? true,
883
                        'remoteId' => $row['parent_remote_id'],
884
                        'sortField' => $row['sort_field'],
885
                        'sortOrder' => $row['sort_order'],
886
                        'priority' => $row['priority'],
887
                        'hidden' => $row['is_hidden'],
888
                        'invisible' => $isInvisible,
889
                    ]
890
                ),
891
                $parentLocationData
892
            );
893
894
            $this->updateNodeAssignment(
895
                (int)$row['contentobject_id'],
896
                (int)$row['parent_node'],
897
                (int)$row['parent_node'],
898
                self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP
899
            );
900
        }
901
    }
902
903
    public function updateLocationsContentVersionNo(int $contentId, int $versionNo): void
904
    {
905
        $query = $this->connection->createQueryBuilder();
906
        $query->update(
907
            self::CONTENT_TREE_TABLE
908
        )->set(
909
            'contentobject_version',
910
            $query->createPositionalParameter($versionNo, ParameterType::INTEGER)
911
        )->where(
912
            $query->expr()->eq(
913
                'contentobject_id',
914
                $contentId
915
            )
916
        );
917
        $query->execute();
918
    }
919
920
    /**
921
     * Search for the main nodeId of $contentId.
922
     */
923
    private function getMainNodeId(int $contentId): ?int
924
    {
925
        $query = $this->connection->createQueryBuilder();
926
        $query
927
            ->select('node_id')
928
            ->from(self::CONTENT_TREE_TABLE)
929
            ->where(
930
                $query->expr()->andX(
931
                    $query->expr()->eq(
932
                        'contentobject_id',
933
                        $query->createPositionalParameter($contentId, ParameterType::INTEGER)
934
                    ),
935
                    $query->expr()->eq(
936
                        'node_id',
937
                        'main_node_id'
938
                    )
939
                )
940
            );
941
        $statement = $query->execute();
942
943
        $result = $statement->fetchColumn();
944
945
        return false !== $result ? (int)$result : null;
946
    }
947
948
    /**
949
     * Updates an existing location.
950
     *
951
     * Will not throw anything if location id is invalid or no entries are affected.
952
     *
953
     * @param \eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct $location
954
     * @param int $locationId
955
     */
956
    public function update(UpdateStruct $location, $locationId): void
957
    {
958
        $query = $this->connection->createQueryBuilder();
959
960
        $query
961
            ->update(self::CONTENT_TREE_TABLE)
962
            ->set(
963
                'priority',
964
                $query->createPositionalParameter($location->priority, ParameterType::INTEGER)
965
            )
966
            ->set(
967
                'remote_id',
968
                $query->createPositionalParameter($location->remoteId, ParameterType::STRING)
969
            )
970
            ->set(
971
                'sort_order',
972
                $query->createPositionalParameter($location->sortOrder, ParameterType::INTEGER)
973
            )
974
            ->set(
975
                'sort_field',
976
                $query->createPositionalParameter($location->sortField, ParameterType::INTEGER)
977
            )
978
            ->where(
979
                $query->expr()->eq(
980
                    'node_id',
981
                    $locationId
982
                )
983
            );
984
        $query->execute();
985
    }
986
987
    public function updatePathIdentificationString($locationId, $parentLocationId, $text): void
988
    {
989
        $parentData = $this->getBasicNodeData($parentLocationId);
990
991
        $newPathIdentificationString = empty($parentData['path_identification_string']) ?
992
            $text :
993
            $parentData['path_identification_string'] . '/' . $text;
994
995
        $query = $this->connection->createQueryBuilder();
996
        $query->update(
997
            self::CONTENT_TREE_TABLE
998
        )->set(
999
            'path_identification_string',
1000
            $query->createPositionalParameter($newPathIdentificationString, ParameterType::STRING)
1001
        )->where(
1002
            $query->expr()->eq(
1003
                'node_id',
1004
                $query->createPositionalParameter($locationId, ParameterType::INTEGER)
1005
            )
1006
        );
1007
        $query->execute();
1008
    }
1009
1010
    /**
1011
     * Deletes ezcontentobject_tree row for given $locationId (node_id).
1012
     *
1013
     * @param mixed $locationId
1014
     */
1015
    public function removeLocation($locationId): void
1016
    {
1017
        $query = $this->connection->createQueryBuilder();
1018
        $query->delete(
1019
            self::CONTENT_TREE_TABLE
1020
        )->where(
1021
            $query->expr()->eq(
1022
                'node_id',
1023
                $query->createPositionalParameter($locationId, ParameterType::INTEGER)
1024
            )
1025
        );
1026
        $query->execute();
1027
    }
1028
1029
    /**
1030
     * Return data of the next in line node to be set as a new main node.
1031
     *
1032
     * This returns lowest node id for content identified by $contentId, and not of
1033
     * the node identified by given $locationId (current main node).
1034
     * Assumes that content has more than one location.
1035
     *
1036
     * @param mixed $contentId
1037
     * @param mixed $locationId
1038
     *
1039
     * @return array
1040
     */
1041
    public function getFallbackMainNodeData($contentId, $locationId): array
1042
    {
1043
        $query = $this->connection->createQueryBuilder();
1044
        $expr = $query->expr();
1045
        $query
1046
            ->select(
1047
                'node_id',
1048
                'contentobject_version',
1049
                'parent_node_id'
1050
            )
1051
            ->from(self::CONTENT_TREE_TABLE)
1052
            ->where(
1053
                $expr->eq(
1054
                    'contentobject_id',
1055
                    $query->createPositionalParameter(
1056
                        $contentId,
1057
                        ParameterType::INTEGER
1058
                    )
1059
                )
1060
            )
1061
            ->andWhere(
1062
                $expr->neq(
1063
                    'node_id',
1064
                    $query->createPositionalParameter(
1065
                        $locationId,
1066
                        ParameterType::INTEGER
1067
                    )
1068
                )
1069
            )
1070
            ->orderBy('node_id', 'ASC')
1071
            ->setMaxResults(1);
1072
1073
        $statement = $query->execute();
1074
1075
        return $statement->fetch(FetchMode::ASSOCIATIVE);
1076
    }
1077
1078
    public function trashLocation(int $locationId): void
1079
    {
1080
        $locationRow = $this->getBasicNodeData($locationId);
1081
1082
        $query = $this->connection->createQueryBuilder();
1083
        $query->insert('ezcontentobject_trash');
1084
1085
        unset($locationRow['contentobject_is_published']);
1086
        $locationRow['trashed'] = time();
1087
        foreach ($locationRow as $key => $value) {
1088
            $query->setValue($key, $query->createPositionalParameter($value));
1089
        }
1090
1091
        $query->execute();
1092
1093
        $this->removeLocation($locationRow['node_id']);
1094
        $this->setContentStatus((int)$locationRow['contentobject_id'], ContentInfo::STATUS_TRASHED);
1095
    }
1096
1097
    public function untrashLocation(int $locationId, ?int $newParentId = null): Location
1098
    {
1099
        $row = $this->loadTrashByLocation($locationId);
1100
1101
        $newLocation = $this->create(
1102
            new CreateStruct(
1103
                [
1104
                    'priority' => $row['priority'],
1105
                    'hidden' => $row['is_hidden'],
1106
                    'invisible' => $row['is_invisible'],
1107
                    'remoteId' => $row['remote_id'],
1108
                    'contentId' => $row['contentobject_id'],
1109
                    'contentVersion' => $row['contentobject_version'],
1110
                    'mainLocationId' => true, // Restored location is always main location
1111
                    'sortField' => $row['sort_field'],
1112
                    'sortOrder' => $row['sort_order'],
1113
                ]
1114
            ),
1115
            $this->getBasicNodeData($newParentId ?? (int)$row['parent_node_id'])
1116
        );
1117
1118
        $this->removeElementFromTrash($locationId);
1119
        $this->setContentStatus((int)$row['contentobject_id'], ContentInfo::STATUS_PUBLISHED);
1120
1121
        return $newLocation;
1122
    }
1123
1124
    private function setContentStatus(int $contentId, int $status): void
1125
    {
1126
        $query = $this->connection->createQueryBuilder();
1127
        $query->update(
1128
            'ezcontentobject'
1129
        )->set(
1130
            'status',
1131
            $query->createPositionalParameter($status, ParameterType::INTEGER)
1132
        )->where(
1133
            $query->expr()->eq(
1134
                'id',
1135
                $query->createPositionalParameter($contentId, ParameterType::INTEGER)
1136
            )
1137
        );
1138
        $query->execute();
1139
    }
1140
1141
    public function loadTrashByLocation(int $locationId): array
1142
    {
1143
        $query = $this->connection->createQueryBuilder();
1144
        $query
1145
            ->select('*')
1146
            ->from('ezcontentobject_trash')
1147
            ->where(
1148
                $query->expr()->eq(
1149
                    'node_id',
1150
                    $query->createPositionalParameter($locationId, ParameterType::INTEGER)
1151
                )
1152
            );
1153
        $statement = $query->execute();
1154
1155
        if ($row = $statement->fetch(FetchMode::ASSOCIATIVE)) {
1156
            return $row;
1157
        }
1158
1159
        throw new NotFound('trash', $locationId);
1160
    }
1161
1162
    public function listTrashed(int $offset, ?int $limit, array $sort = null): array
1163
    {
1164
        $query = $this->connection->createQueryBuilder();
1165
        $query
1166
            ->select('*')
1167
            ->from('ezcontentobject_trash');
1168
1169
        $sort = $sort ?: [];
1170
        foreach ($sort as $condition) {
1171
            if (!isset(self::SORT_CLAUSE_TARGET_MAP[$condition->target])) {
1172
                // Only handle location related sort clause targets. The others
1173
                // require data aggregation which is not sensible here.
1174
                // Since also criteria are yet ignored, because they are
1175
                // simply not used yet in eZ Platform, we skip that for now.
1176
                throw new RuntimeException('Unhandled sort clause: ' . get_class($condition));
1177
            }
1178
            $query->addOrderBy(
1179
                self::SORT_CLAUSE_TARGET_MAP[$condition->target],
1180
                $condition->direction === Query::SORT_ASC ? 'ASC' : 'DESC'
1181
            );
1182
        }
1183
1184
        if ($limit !== null) {
1185
            $query->setMaxResults($limit);
1186
            $query->setFirstResult($offset);
1187
        }
1188
1189
        $statement = $query->execute();
1190
1191
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
1192
    }
1193
1194
    public function countTrashed(): int
1195
    {
1196
        $query = $this->connection->createQueryBuilder()
1197
            ->select($this->dbPlatform->getCountExpression('node_id'))
1198
            ->from('ezcontentobject_trash');
1199
1200
        return (int)$query->execute()->fetchColumn();
1201
    }
1202
1203
    /**
1204
     * Removes every entries in the trash.
1205
     * Will NOT remove associated content objects nor attributes.
1206
     *
1207
     * Basically truncates ezcontentobject_trash table.
1208
     */
1209
    public function cleanupTrash(): void
1210
    {
1211
        $query = $this->connection->createQueryBuilder();
1212
        $query->delete('ezcontentobject_trash');
1213
        $query->execute();
1214
    }
1215
1216
    public function removeElementFromTrash(int $id): void
1217
    {
1218
        $query = $this->connection->createQueryBuilder();
1219
        $query
1220
            ->delete('ezcontentobject_trash')
1221
            ->where(
1222
                $query->expr()->eq(
1223
                    'node_id',
1224
                    $query->createPositionalParameter($id, ParameterType::INTEGER)
1225
                )
1226
            );
1227
        $query->execute();
1228
    }
1229
1230
    public function setSectionForSubtree(string $pathString, int $sectionId): bool
1231
    {
1232
        $selectContentIdsQuery = $this->connection->createQueryBuilder();
1233
        $selectContentIdsQuery
1234
            ->select('t.contentobject_id')
1235
            ->from(self::CONTENT_TREE_TABLE, 't')
1236
            ->where(
1237
                $selectContentIdsQuery->expr()->like(
1238
                    't.path_string',
1239
                    $selectContentIdsQuery->createPositionalParameter("{$pathString}%")
1240
                )
1241
            );
1242
1243
        $contentIds = array_map(
1244
            'intval',
1245
            $selectContentIdsQuery->execute()->fetchAll(FetchMode::COLUMN)
1246
        );
1247
1248
        if (empty($contentIds)) {
1249
            return false;
1250
        }
1251
1252
        $updateSectionQuery = $this->connection->createQueryBuilder();
1253
        $updateSectionQuery
1254
            ->update('ezcontentobject')
1255
            ->set(
1256
                'section_id',
1257
                $updateSectionQuery->createPositionalParameter($sectionId, ParameterType::INTEGER)
1258
            )
1259
            ->where(
1260
                $updateSectionQuery->expr()->in(
1261
                    'id',
1262
                    $contentIds
1263
                )
1264
            );
1265
        $affectedRows = $updateSectionQuery->execute();
1266
1267
        return $affectedRows > 0;
1268
    }
1269
1270
    public function countLocationsByContentId(int $contentId): int
1271
    {
1272
        $query = $this->connection->createQueryBuilder();
1273
        $query
1274
            ->select(
1275
                $this->dbPlatform->getCountExpression('*')
1276
            )
1277
            ->from(self::CONTENT_TREE_TABLE)
1278
            ->where(
1279
                $query->expr()->eq(
1280
                    'contentobject_id',
1281
                    $query->createPositionalParameter($contentId, ParameterType::INTEGER)
1282
                )
1283
            );
1284
        $stmt = $query->execute();
1285
1286
        return (int)$stmt->fetchColumn();
1287
    }
1288
1289
    public function changeMainLocation(
1290
        int $contentId,
1291
        int $locationId,
1292
        int $versionNo,
1293
        int $parentLocationId
1294
    ): void {
1295
        // Update ezcontentobject_tree table
1296
        $query = $this->connection->createQueryBuilder();
1297
        $query
1298
            ->update(self::CONTENT_TREE_TABLE)
1299
            ->set(
1300
                'main_node_id',
1301
                $query->createPositionalParameter($locationId, ParameterType::INTEGER)
1302
            )
1303
            ->where(
1304
                $query->expr()->eq(
1305
                    'contentobject_id',
1306
                    $query->createPositionalParameter($contentId, ParameterType::INTEGER)
1307
                )
1308
            )
1309
        ;
1310
        $query->execute();
1311
1312
        // Update is_main in eznode_assignment table
1313
        $this->setIsMainForContentVersionParentNodeAssignment(
1314
            $contentId,
1315
            $versionNo,
1316
            $parentLocationId
1317
        );
1318
    }
1319
1320
    public function countAllLocations(): int
1321
    {
1322
        $query = $this->createNodeQueryBuilder(['count(node_id)']);
1323
        // exclude absolute Root Location (not to be confused with SiteAccess Tree Root)
1324
        $query->where($query->expr()->neq('node_id', 'parent_node_id'));
1325
1326
        $statement = $query->execute();
1327
1328
        return (int) $statement->fetch(FetchMode::COLUMN);
1329
    }
1330
1331
    public function loadAllLocationsData(int $offset, int $limit): array
1332
    {
1333
        $query = $this
1334
            ->createNodeQueryBuilder(
1335
                [
1336
                    'node_id',
1337
                    'priority',
1338
                    'is_hidden',
1339
                    'is_invisible',
1340
                    'remote_id',
1341
                    'contentobject_id',
1342
                    'parent_node_id',
1343
                    'path_identification_string',
1344
                    'path_string',
1345
                    'depth',
1346
                    'sort_field',
1347
                    'sort_order',
1348
                ]
1349
            );
1350
        $query
1351
            // exclude absolute Root Location (not to be confused with SiteAccess Tree Root)
1352
            ->where($query->expr()->neq('node_id', 'parent_node_id'))
1353
            ->setFirstResult($offset)
1354
            ->setMaxResults($limit)
1355
            ->orderBy('depth', 'ASC')
1356
            ->addOrderBy('node_id', 'ASC')
1357
        ;
1358
1359
        $statement = $query->execute();
1360
1361
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
1362
    }
1363
1364
    /**
1365
     * Create QueryBuilder for selecting Location (node) data.
1366
     *
1367
     * @param array $columns column or expression list
1368
     * @param array|null $translations Filters on language mask of content if provided.
1369
     * @param bool $useAlwaysAvailable Respect always available flag on content when filtering on $translations.
1370
     *
1371
     * @return \Doctrine\DBAL\Query\QueryBuilder
1372
     */
1373
    private function createNodeQueryBuilder(
1374
        array $columns,
1375
        array $translations = null,
1376
        bool $useAlwaysAvailable = true
1377
    ): QueryBuilder {
1378
        $queryBuilder = $this->connection->createQueryBuilder();
1379
        $queryBuilder
1380
            ->select($columns)
1381
            ->from(self::CONTENT_TREE_TABLE, 't')
1382
        ;
1383
1384
        if (!empty($translations)) {
1385
            $this->appendContentItemTranslationsConstraint($queryBuilder, $translations, $useAlwaysAvailable);
1386
        }
1387
1388
        return $queryBuilder;
1389
    }
1390
1391
    private function appendContentItemTranslationsConstraint(
1392
        QueryBuilder $queryBuilder,
1393
        array $translations,
1394
        bool $useAlwaysAvailable
1395
    ): void {
1396
        $expr = $queryBuilder->expr();
1397
        try {
1398
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
1399
                $translations,
1400
                $useAlwaysAvailable
1401
            );
1402
        } catch (NotFoundException $e) {
1403
            return;
1404
        }
1405
1406
        $queryBuilder->leftJoin(
1407
            't',
1408
            'ezcontentobject',
1409
            'c',
1410
            $expr->eq('t.contentobject_id', 'c.id')
1411
        );
1412
1413
        $queryBuilder->andWhere(
1414
            $expr->orX(
1415
                $expr->gt(
1416
                    $this->dbPlatform->getBitAndComparisonExpression('c.language_mask', $mask),
1417
                    0
1418
                ),
1419
                // Root location doesn't have language mask
1420
                $expr->eq(
1421
                    't.node_id', 't.parent_node_id'
1422
                )
1423
            )
1424
        );
1425
    }
1426
1427
    /**
1428
     * Mark eznode_assignment entry, identified by Content ID and Version ID, as main for the given
1429
     * parent Location ID.
1430
     *
1431
     * **NOTE**: The method erases is_main from the other entries related to Content and Version IDs
1432
     */
1433
    private function setIsMainForContentVersionParentNodeAssignment(
1434
        int $contentId,
1435
        int $versionNo,
1436
        int $parentLocationId
1437
    ): void {
1438
        $query = $this->connection->createQueryBuilder();
1439
        $query
1440
            ->update('eznode_assignment')
1441
            ->set(
1442
                'is_main',
1443
                // set is_main = 1 only for current parent, set 0 for other entries
1444
                'CASE WHEN parent_node <> :parent_location_id THEN 0 ELSE 1 END'
1445
            )
1446
            ->where('contentobject_id = :content_id')
1447
            ->andWhere('contentobject_version = :version_no')
1448
            ->setParameter('parent_location_id', $parentLocationId, ParameterType::INTEGER)
1449
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1450
            ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1451
1452
        $query->execute();
1453
    }
1454
1455
    /**
1456
     * @param array $parentNode raw Location data
1457
     */
1458
    private function insertLocationIntoContentTree(
1459
        CreateStruct $createStruct,
1460
        array $parentNode
1461
    ): Location {
1462
        $location = new Location();
1463
        $query = $this->connection->createQueryBuilder();
1464
        $query
1465
            ->insert(self::CONTENT_TREE_TABLE)
1466
            ->values(
1467
                [
1468
                    'contentobject_id' => ':content_id',
1469
                    'contentobject_is_published' => ':is_published',
1470
                    'contentobject_version' => ':version_no',
1471
                    'depth' => ':depth',
1472
                    'is_hidden' => ':is_hidden',
1473
                    'is_invisible' => ':is_invisible',
1474
                    'modified_subnode' => ':modified_subnode',
1475
                    'parent_node_id' => ':parent_node_id',
1476
                    'path_string' => ':path_string',
1477
                    'priority' => ':priority',
1478
                    'remote_id' => ':remote_id',
1479
                    'sort_field' => ':sort_field',
1480
                    'sort_order' => ':sort_order',
1481
                ]
1482
            )
1483
            ->setParameters(
1484
                [
1485
                    'content_id' => $location->contentId = $createStruct->contentId,
1486
                    'is_published' => 1,
1487
                    'version_no' => $createStruct->contentVersion,
1488
                    'depth' => $location->depth = $parentNode['depth'] + 1,
1489
                    'is_hidden' => $location->hidden = $createStruct->hidden,
1490
                    'is_invisible' => $location->invisible = $createStruct->invisible,
1491
                    'modified_subnode' => time(),
1492
                    'parent_node_id' => $location->parentId = $parentNode['node_id'],
1493
                    'path_string' => '', // Set later
1494
                    'priority' => $location->priority = $createStruct->priority,
1495
                    'remote_id' => $location->remoteId = $createStruct->remoteId,
1496
                    'sort_field' => $location->sortField = $createStruct->sortField,
1497
                    'sort_order' => $location->sortOrder = $createStruct->sortOrder,
1498
                ],
1499
                [
1500
                    'contentobject_id' => ParameterType::INTEGER,
1501
                    'contentobject_is_published' => ParameterType::INTEGER,
1502
                    'contentobject_version' => ParameterType::INTEGER,
1503
                    'depth' => ParameterType::INTEGER,
1504
                    'is_hidden' => ParameterType::INTEGER,
1505
                    'is_invisible' => ParameterType::INTEGER,
1506
                    'modified_subnode' => ParameterType::INTEGER,
1507
                    'parent_node_id' => ParameterType::INTEGER,
1508
                    'path_string' => ParameterType::STRING,
1509
                    'priority' => ParameterType::INTEGER,
1510
                    'remote_id' => ParameterType::STRING,
1511
                    'sort_field' => ParameterType::INTEGER,
1512
                    'sort_order' => ParameterType::INTEGER,
1513
                ]
1514
            );
1515
        $query->execute();
1516
1517
        $location->id = (int)$this->connection->lastInsertId(self::CONTENT_TREE_SEQ);
1518
1519
        return $location;
1520
    }
1521
}
1522