DoctrineDatabase::countAllLocations()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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