Completed
Push — master ( 5acaba...f6e111 )
by Łukasz
31:51
created

DoctrineDatabase::create()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 81
rs 8.4145
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * File containing the DoctrineDatabase Location Gateway class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
10
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\FetchMode;
13
use Doctrine\DBAL\Query\QueryBuilder;
14
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator;
15
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway;
16
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
17
use eZ\Publish\Core\Persistence\Database\SelectQuery;
18
use eZ\Publish\Core\Persistence\Database\Query as DatabaseQuery;
19
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
20
use eZ\Publish\SPI\Persistence\Content\Location;
21
use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
22
use eZ\Publish\SPI\Persistence\Content\Location\CreateStruct;
23
use eZ\Publish\API\Repository\Values\Content\Query;
24
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
25
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
26
use RuntimeException;
27
use PDO;
28
29
/**
30
 * Location gateway implementation using the Doctrine database.
31
 */
32
class DoctrineDatabase extends Gateway
33
{
34
    /**
35
     * 2^30, since PHP_INT_MAX can cause overflows in DB systems, if PHP is run
36
     * on 64 bit systems.
37
     */
38
    const MAX_LIMIT = 1073741824;
39
40
    /**
41
     * Database handler.
42
     *
43
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
44
     */
45
    protected $handler;
46
47
    /**
48
     * @var \Doctrine\DBAL\Connection
49
     */
50
    protected $connection;
51
52
    /**
53
     * Language mask generator.
54
     *
55
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
56
     */
57
    protected $languageMaskGenerator;
58
59
    /**
60
     * Construct from database handler.
61
     *
62
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $handler
63
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
64
     */
65
    public function __construct(DatabaseHandler $handler, MaskGenerator $languageMaskGenerator)
66
    {
67
        $this->handler = $handler;
68
        $this->connection = $handler->getConnection();
69
        $this->languageMaskGenerator = $languageMaskGenerator;
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 View Code Duplication
    public function getBasicNodeData($nodeId, array $translations = null, bool $useAlwaysAvailable = true)
76
    {
77
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
78
        $q->where(
79
            $q->expr()->eq('t.node_id', $q->createNamedParameter($nodeId, PDO::PARAM_INT))
80
        );
81
82
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
83
            return $row;
84
        }
85
86
        throw new NotFound('location', $nodeId);
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public function getNodeDataList(array $locationIds, array $translations = null, bool $useAlwaysAvailable = true): iterable
93
    {
94
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
95
        $q->where(
96
            $q->expr()->in(
97
                't.node_id',
98
                $q->createNamedParameter($locationIds, Connection::PARAM_INT_ARRAY)
99
            )
100
        );
101
102
        return $q->execute()->fetchAll(FetchMode::ASSOCIATIVE);
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 View Code Duplication
    public function getBasicNodeDataByRemoteId($remoteId, array $translations = null, bool $useAlwaysAvailable = true)
109
    {
110
        $q = $this->createNodeQueryBuilder($translations, $useAlwaysAvailable);
111
        $q->where(
112
            $q->expr()->eq('t.remote_id', $q->createNamedParameter($remoteId, PDO::PARAM_STR))
113
        );
114
115
        if ($row = $q->execute()->fetch(FetchMode::ASSOCIATIVE)) {
116
            return $row;
117
        }
118
119
        throw new NotFound('location', $remoteId);
120
    }
121
122
    /**
123
     * Loads data for all Locations for $contentId, optionally only in the
124
     * subtree starting at $rootLocationId.
125
     *
126
     * @param int $contentId
127
     * @param int $rootLocationId
128
     *
129
     * @return array
130
     */
131
    public function loadLocationDataByContent($contentId, $rootLocationId = null)
132
    {
133
        $query = $this->handler->createSelectQuery();
134
        $query
135
            ->select('*')
136
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
137
            ->where(
138
                $query->expr->eq(
139
                    $this->handler->quoteColumn('contentobject_id'),
140
                    $query->bindValue($contentId)
141
                )
142
            );
143
144
        if ($rootLocationId !== null) {
145
            $this->applySubtreeLimitation($query, $rootLocationId);
146
        }
147
148
        $statement = $query->prepare();
149
        $statement->execute();
150
151
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
152
    }
153
154
    /**
155
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway::loadParentLocationsDataForDraftContent
156
     */
157
    public function loadParentLocationsDataForDraftContent($contentId, $drafts = null)
158
    {
159
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
160
        $query = $this->handler->createSelectQuery();
161
        $query->selectDistinct(
162
            'ezcontentobject_tree.*'
163
        )->from(
164
            $this->handler->quoteTable('ezcontentobject_tree')
165
        )->innerJoin(
166
            $this->handler->quoteTable('eznode_assignment'),
167
            $query->expr->lAnd(
168
                $query->expr->eq(
169
                    $this->handler->quoteColumn('node_id', 'ezcontentobject_tree'),
170
                    $this->handler->quoteColumn('parent_node', 'eznode_assignment')
171
                ),
172
                $query->expr->eq(
173
                    $this->handler->quoteColumn('contentobject_id', 'eznode_assignment'),
174
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
175
                ),
176
                $query->expr->eq(
177
                    $this->handler->quoteColumn('op_code', 'eznode_assignment'),
178
                    $query->bindValue(self::NODE_ASSIGNMENT_OP_CODE_CREATE, null, \PDO::PARAM_INT)
179
                )
180
            )
181
        )->innerJoin(
182
            $this->handler->quoteTable('ezcontentobject'),
183
            $query->expr->lAnd(
184
                $query->expr->lOr(
185
                    $query->expr->eq(
186
                        $this->handler->quoteColumn('contentobject_id', 'eznode_assignment'),
187
                        $this->handler->quoteColumn('id', 'ezcontentobject')
188
                    )
189
                ),
190
                $query->expr->eq(
191
                    $this->handler->quoteColumn('status', 'ezcontentobject'),
192
                    $query->bindValue(ContentInfo::STATUS_DRAFT, null, \PDO::PARAM_INT)
193
                )
194
            )
195
        );
196
197
        $statement = $query->prepare();
198
        $statement->execute();
199
200
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
201
    }
202
203
    /**
204
     * Find all content in the given subtree.
205
     *
206
     * @param mixed $sourceId
207
     * @param bool $onlyIds
208
     *
209
     * @return array
210
     */
211
    public function getSubtreeContent($sourceId, $onlyIds = false)
212
    {
213
        $query = $this->handler->createSelectQuery();
214
        $query->select($onlyIds ? 'node_id, contentobject_id, depth' : '*')->from(
215
            $this->handler->quoteTable('ezcontentobject_tree')
216
        );
217
        $this->applySubtreeLimitation($query, $sourceId);
218
        $query->orderBy(
219
            $this->handler->quoteColumn('depth', 'ezcontentobject_tree')
220
        )->orderBy(
221
            $this->handler->quoteColumn('node_id', 'ezcontentobject_tree')
222
        );
223
        $statement = $query->prepare();
224
        $statement->execute();
225
226
        $results = $statement->fetchAll($onlyIds ? (PDO::FETCH_COLUMN | PDO::FETCH_GROUP) : PDO::FETCH_ASSOC);
227
        // array_map() is used to to map all elements stored as $results[$i][0] to $results[$i]
228
        return $onlyIds ? array_map('reset', $results) : $results;
229
    }
230
231
    /**
232
     * Limits the given $query to the subtree starting at $rootLocationId.
233
     *
234
     * @param \eZ\Publish\Core\Persistence\Database\Query $query
235
     * @param string $rootLocationId
236
     */
237
    protected function applySubtreeLimitation(DatabaseQuery $query, $rootLocationId)
238
    {
239
        $query->where(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\Core\Persistence\Database\Query as the method where() does only exist in the following implementations of said interface: eZ\Publish\Core\Persiste...ine\DeleteDoctrineQuery, eZ\Publish\Core\Persiste...ine\SelectDoctrineQuery, eZ\Publish\Core\Persiste...\SubselectDoctrineQuery, eZ\Publish\Core\Persiste...ine\UpdateDoctrineQuery.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
240
            $query->expr->like(
241
                $this->handler->quoteColumn('path_string', 'ezcontentobject_tree'),
242
                $query->bindValue('%/' . $rootLocationId . '/%')
243
            )
244
        );
245
    }
246
247
    /**
248
     * Returns data for the first level children of the location identified by given $locationId.
249
     *
250
     * @param mixed $locationId
251
     *
252
     * @return array
253
     */
254
    public function getChildren($locationId)
255
    {
256
        $query = $this->handler->createSelectQuery();
257
        $query->select('*')->from(
258
            $this->handler->quoteTable('ezcontentobject_tree')
259
        )->where(
260
            $query->expr->eq(
261
                $this->handler->quoteColumn('parent_node_id', 'ezcontentobject_tree'),
262
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
263
            )
264
        );
265
        $statement = $query->prepare();
266
        $statement->execute();
267
268
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
269
    }
270
271
    /**
272
     * Update path strings to move nodes in the ezcontentobject_tree table.
273
     *
274
     * This query can likely be optimized to use some more advanced string
275
     * operations, which then depend on the respective database.
276
     *
277
     * @todo optimize
278
     *
279
     * @param array $sourceNodeData
280
     * @param array $destinationNodeData
281
     */
282
    public function moveSubtreeNodes(array $sourceNodeData, array $destinationNodeData)
283
    {
284
        $fromPathString = $sourceNodeData['path_string'];
285
286
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
287
        $query = $this->handler->createSelectQuery();
288
        $query
289
            ->select(
290
                $this->handler->quoteColumn('node_id'),
291
                $this->handler->quoteColumn('parent_node_id'),
292
                $this->handler->quoteColumn('path_string'),
293
                $this->handler->quoteColumn('path_identification_string')
294
            )
295
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
296
            ->where(
297
                $query->expr->like(
298
                    $this->handler->quoteColumn('path_string'),
299
                    $query->bindValue($fromPathString . '%')
300
                )
301
            );
302
        $statement = $query->prepare();
303
        $statement->execute();
304
305
        $rows = $statement->fetchAll();
306
        $oldParentPathString = implode('/', array_slice(explode('/', $fromPathString), 0, -2)) . '/';
307
        $oldParentPathIdentificationString = implode(
308
            '/',
309
            array_slice(explode('/', $sourceNodeData['path_identification_string']), 0, -1)
310
        );
311
312
        foreach ($rows as $row) {
313
            // Prefixing ensures correct replacement when old parent is root node
314
            $newPathString = str_replace(
315
                'prefix' . $oldParentPathString,
316
                $destinationNodeData['path_string'],
317
                'prefix' . $row['path_string']
318
            );
319
            $newPathIdentificationString = str_replace(
320
                'prefix' . $oldParentPathIdentificationString,
321
                $destinationNodeData['path_identification_string'] . '/',
322
                'prefix' . $row['path_identification_string']
323
            );
324
325
            $newParentId = $row['parent_node_id'];
326
            if ($row['path_string'] === $fromPathString) {
327
                $newParentId = (int)implode('', array_slice(explode('/', $newPathString), -3, 1));
328
            }
329
330
            /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
331
            $query = $this->handler->createUpdateQuery();
332
            $query
333
                ->update($this->handler->quoteTable('ezcontentobject_tree'))
334
                ->set(
335
                    $this->handler->quoteColumn('path_string'),
336
                    $query->bindValue($newPathString)
337
                )
338
                ->set(
339
                    $this->handler->quoteColumn('path_identification_string'),
340
                    $query->bindValue($newPathIdentificationString)
341
                )
342
                ->set(
343
                    $this->handler->quoteColumn('depth'),
344
                    $query->bindValue(substr_count($newPathString, '/') - 2)
345
                )
346
                ->set(
347
                    $this->handler->quoteColumn('parent_node_id'),
348
                    $query->bindValue($newParentId)
349
                );
350
351
            if ($destinationNodeData['is_hidden'] || $destinationNodeData['is_invisible']) {
352
                // CASE 1: Mark whole tree as invisible if destination is invisible and/or hidden
353
                $query->set(
354
                    $this->handler->quoteColumn('is_invisible'),
355
                    $query->bindValue(1)
356
                );
357
            } elseif (!$sourceNodeData['is_hidden'] && $sourceNodeData['is_invisible']) {
358
                // CASE 2: source is only invisible, we will need to re-calculate whole moved tree visibility
359
                $query->set(
360
                    $this->handler->quoteColumn('is_invisible'),
361
                    $query->bindValue($this->isHiddenByParent($newPathString, $rows) ? 1 : 0)
362
                );
363
            } else {
364
                // CASE 3: keep invisible flags as is (source is either hidden or not hidden/invisible at all)
365
            }
366
367
            $query->where(
368
                    $query->expr->eq(
369
                        $this->handler->quoteColumn('node_id'),
370
                        $query->bindValue($row['node_id'])
371
                    )
372
                );
373
            $query->prepare()->execute();
374
        }
375
    }
376
377
    private function isHiddenByParent($pathString, array $rows)
378
    {
379
        $parentNodeIds = explode('/', trim($pathString, '/'));
380
        array_pop($parentNodeIds); // remove self
381
        foreach ($rows as $row) {
382
            if ($row['is_hidden'] && in_array($row['node_id'], $parentNodeIds)) {
383
                return true;
384
            }
385
        }
386
387
        return false;
388
    }
389
390
    /**
391
     * Updated subtree modification time for all nodes on path.
392
     *
393
     * @param string $pathString
394
     * @param int|null $timestamp
395
     */
396
    public function updateSubtreeModificationTime($pathString, $timestamp = null)
397
    {
398
        $nodes = array_filter(explode('/', $pathString));
399
        $query = $this->handler->createUpdateQuery();
400
        $query
401
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
402
            ->set(
403
                $this->handler->quoteColumn('modified_subnode'),
404
                $query->bindValue(
405
                    $timestamp ?: time()
406
                )
407
            )
408
            ->where(
409
                $query->expr->in(
410
                    $this->handler->quoteColumn('node_id'),
411
                    $nodes
412
                )
413
            );
414
        $query->prepare()->execute();
415
    }
416
417
    /**
418
     * Sets a location to be hidden, and it self + all children to invisible.
419
     *
420
     * @param string $pathString
421
     */
422
    public function hideSubtree($pathString)
423
    {
424
        $this->setNodeWithChildrenInvisible($pathString);
425
        $this->setNodeHidden($pathString);
426
    }
427
428
    /**
429
     * @param string $pathString
430
     **/
431
    public function setNodeWithChildrenInvisible(string $pathString): void
432
    {
433
        $query = $this->handler->createUpdateQuery();
434
        $query
435
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
436
            ->set(
437
                $this->handler->quoteColumn('is_invisible'),
438
                $query->bindValue(1)
439
            )
440
            ->set(
441
                $this->handler->quoteColumn('modified_subnode'),
442
                $query->bindValue(time())
443
            )
444
            ->where(
445
                $query->expr->like(
446
                    $this->handler->quoteColumn('path_string'),
447
                    $query->bindValue($pathString . '%')
448
                )
449
            );
450
451
        $query->prepare()->execute();
452
    }
453
454
    /**
455
     * @param string $pathString
456
     **/
457
    public function setNodeHidden(string $pathString): void
458
    {
459
        $this->setNodeHiddenStatus($pathString, true);
460
    }
461
462
    /**
463
     * @param string $pathString
464
     * @param bool $isHidden
465
     */
466 View Code Duplication
    private function setNodeHiddenStatus(string $pathString, bool $isHidden): void
467
    {
468
        $query = $this->handler->createUpdateQuery();
469
        $query
470
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
471
            ->set(
472
                $this->handler->quoteColumn('is_hidden'),
473
                $query->bindValue((int) $isHidden)
474
            )
475
            ->where(
476
                $query->expr->eq(
477
                    $this->handler->quoteColumn('path_string'),
478
                    $query->bindValue($pathString)
479
                )
480
            );
481
482
        $query->prepare()->execute();
483
    }
484
485
    /**
486
     * Sets a location to be unhidden, and self + children to visible unless a parent is hiding the tree.
487
     * If not make sure only children down to first hidden node is marked visible.
488
     *
489
     * @param string $pathString
490
     */
491
    public function unHideSubtree($pathString)
492
    {
493
        $this->setNodeUnhidden($pathString);
494
        $this->setNodeWithChildrenVisible($pathString);
495
    }
496
497
    /**
498
     * Sets a location + children to visible unless a parent is hiding the tree.
499
     *
500
     * @param string $pathString
501
     **/
502
    public function setNodeWithChildrenVisible(string $pathString): void
503
    {
504
        // Check if any parent nodes are explicitly hidden
505
        $query = $this->handler->createSelectQuery();
506
        $query
507
            ->select($this->handler->quoteColumn('path_string'))
508
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
509
            ->where(
510
                $query->expr->lAnd(
511
                    $query->expr->eq(
512
                        $this->handler->quoteColumn('is_hidden'),
513
                        $query->bindValue(1)
514
                    ),
515
                    $query->expr->in(
516
                        $this->handler->quoteColumn('node_id'),
517
                        array_filter(explode('/', $pathString))
518
                    )
519
                )
520
            );
521
522
        $statement = $query->prepare();
523
        $statement->execute();
524
        if (count($statement->fetchAll(\PDO::FETCH_COLUMN))) {
525
            // There are parent nodes set hidden, so that we can skip marking
526
            // something visible again.
527
            return;
528
        }
529
530
        // Find nodes of explicitly hidden subtrees in the subtree which
531
        // should be unhidden
532
        $query = $this->handler->createSelectQuery();
533
        $query
534
            ->select($this->handler->quoteColumn('path_string'))
535
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
536
            ->where(
537
                $query->expr->lAnd(
538
                    $query->expr->eq(
539
                        $this->handler->quoteColumn('is_hidden'),
540
                        $query->bindValue(1)
541
                    ),
542
                    $query->expr->like(
543
                        $this->handler->quoteColumn('path_string'),
544
                        $query->bindValue($pathString . '%')
545
                    )
546
                )
547
            );
548
        $statement = $query->prepare();
549
        $statement->execute();
550
        $hiddenSubtrees = $statement->fetchAll(\PDO::FETCH_COLUMN);
551
552
        $query = $this->handler->createUpdateQuery();
553
        $query
554
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
555
            ->set(
556
                $this->handler->quoteColumn('is_invisible'),
557
                $query->bindValue(0)
558
            )
559
            ->set(
560
                $this->handler->quoteColumn('modified_subnode'),
561
                $query->bindValue(time())
562
            );
563
564
        // Build where expression selecting the nodes, which should be made
565
        // visible again
566
        $where = $query->expr->like(
567
            $this->handler->quoteColumn('path_string'),
568
            $query->bindValue($pathString . '%')
569
        );
570
        if (count($hiddenSubtrees)) {
571
            $handler = $this->handler;
572
            $where = $query->expr->lAnd(
573
                $where,
574
                $query->expr->lAnd(
575
                    array_map(
576
                        function ($pathString) use ($query, $handler) {
577
                            return $query->expr->not(
578
                                $query->expr->like(
579
                                    $handler->quoteColumn('path_string'),
580
                                    $query->bindValue($pathString . '%')
581
                                )
582
                            );
583
                        },
584
                        $hiddenSubtrees
585
                    )
586
                )
587
            );
588
        }
589
        $query->where($where);
590
        $query->prepare()->execute();
591
    }
592
593
    /**
594
     * Sets location to be unhidden.
595
     *
596
     * @param string $pathString
597
     **/
598
    public function setNodeUnhidden(string $pathString): void
599
    {
600
        $this->setNodeHiddenStatus($pathString, false);
601
    }
602
603
    /**
604
     * Swaps the content object being pointed to by a location object.
605
     *
606
     * Make the location identified by $locationId1 refer to the Content
607
     * referred to by $locationId2 and vice versa.
608
     *
609
     * @param int $locationId1
610
     * @param int $locationId2
611
     *
612
     * @return bool
613
     */
614
    public function swap(int $locationId1, int $locationId2): bool
615
    {
616
        $queryBuilder = $this->connection->createQueryBuilder();
617
        $expr = $queryBuilder->expr();
618
        $queryBuilder
619
            ->select('node_id', 'main_node_id', 'contentobject_id', 'contentobject_version')
620
            ->from('ezcontentobject_tree')
621
            ->where(
622
                $expr->in(
623
                    'node_id',
624
                    ':locationIds'
625
                )
626
            )
627
            ->setParameter('locationIds', [$locationId1, $locationId2], Connection::PARAM_INT_ARRAY)
628
        ;
629
        $statement = $queryBuilder->execute();
630
        $contentObjects = [];
631
        foreach ($statement->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
632
            $row['is_main_node'] = (int)$row['main_node_id'] === (int)$row['node_id'];
633
            $contentObjects[$row['node_id']] = $row;
634
        }
635
636
        if (!isset($contentObjects[$locationId1], $contentObjects[$locationId2])) {
637
            throw new RuntimeException(
638
                sprintf(
639
                    '%s: failed to fetch either Location %d or Location %d',
640
                    __METHOD__,
641
                    $locationId1,
642
                    $locationId2
643
                )
644
            );
645
        }
646
        $content1data = $contentObjects[$locationId1];
647
        $content2data = $contentObjects[$locationId2];
648
649
        $queryBuilder = $this->connection->createQueryBuilder();
650
        $queryBuilder
651
            ->update('ezcontentobject_tree')
652
            ->set('contentobject_id', ':contentId')
653
            ->set('contentobject_version', ':versionNo')
654
            ->set('main_node_id', ':mainNodeId')
655
            ->where(
656
                $expr->eq('node_id', ':locationId')
657
            );
658
659
        $queryBuilder
660
            ->setParameter(':contentId', $content2data['contentobject_id'])
661
            ->setParameter(':versionNo', $content2data['contentobject_version'])
662
            ->setParameter(
663
                ':mainNodeId',
664
                // make main Location main again, preserve main Location id of non-main one
665
                $content2data['is_main_node']
666
                    ? $content1data['node_id']
667
                    : $content2data['main_node_id']
668
            )
669
            ->setParameter('locationId', $locationId1);
670
671
        // update Location 1 entry
672
        $queryBuilder->execute();
673
674
        $queryBuilder
675
            ->setParameter(':contentId', $content1data['contentobject_id'])
676
            ->setParameter(':versionNo', $content1data['contentobject_version'])
677
            ->setParameter(
678
                ':mainNodeId',
679
                $content1data['is_main_node']
680
                    // make main Location main again, preserve main Location id of non-main one
681
                    ? $content2data['node_id']
682
                    : $content1data['main_node_id']
683
            )
684
            ->setParameter('locationId', $locationId2);
685
686
        // update Location 2 entry
687
        $queryBuilder->execute();
688
689
        return true;
690
    }
691
692
    /**
693
     * Creates a new location in given $parentNode.
694
     *
695
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
696
     * @param array $parentNode
697
     *
698
     * @return \eZ\Publish\SPI\Persistence\Content\Location
699
     */
700
    public function create(CreateStruct $createStruct, array $parentNode)
701
    {
702
        $location = new Location();
703
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
704
        $query = $this->handler->createInsertQuery();
705
        $query
706
            ->insertInto($this->handler->quoteTable('ezcontentobject_tree'))
707
            ->set(
708
                $this->handler->quoteColumn('contentobject_id'),
709
                $query->bindValue($location->contentId = $createStruct->contentId, null, \PDO::PARAM_INT)
710
            )->set(
711
                $this->handler->quoteColumn('contentobject_is_published'),
712
                $query->bindValue(1, null, \PDO::PARAM_INT)
713
            )->set(
714
                $this->handler->quoteColumn('contentobject_version'),
715
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
716
            )->set(
717
                $this->handler->quoteColumn('depth'),
718
                $query->bindValue($location->depth = $parentNode['depth'] + 1, null, \PDO::PARAM_INT)
719
            )->set(
720
                $this->handler->quoteColumn('is_hidden'),
721
                $query->bindValue($location->hidden = $createStruct->hidden, null, \PDO::PARAM_INT)
722
            )->set(
723
                $this->handler->quoteColumn('is_invisible'),
724
                $query->bindValue($location->invisible = $createStruct->invisible, null, \PDO::PARAM_INT)
725
            )->set(
726
                $this->handler->quoteColumn('modified_subnode'),
727
                $query->bindValue(time(), null, \PDO::PARAM_INT)
728
            )->set(
729
                $this->handler->quoteColumn('node_id'),
730
                $this->handler->getAutoIncrementValue('ezcontentobject_tree', 'node_id')
731
            )->set(
732
                $this->handler->quoteColumn('parent_node_id'),
733
                $query->bindValue($location->parentId = $parentNode['node_id'], null, \PDO::PARAM_INT)
734
            )->set(
735
                $this->handler->quoteColumn('path_identification_string'),
736
                $query->bindValue($location->pathIdentificationString = $createStruct->pathIdentificationString, null, \PDO::PARAM_STR)
737
            )->set(
738
                $this->handler->quoteColumn('path_string'),
739
                $query->bindValue('dummy') // Set later
740
            )->set(
741
                $this->handler->quoteColumn('priority'),
742
                $query->bindValue($location->priority = $createStruct->priority, null, \PDO::PARAM_INT)
743
            )->set(
744
                $this->handler->quoteColumn('remote_id'),
745
                $query->bindValue($location->remoteId = $createStruct->remoteId, null, \PDO::PARAM_STR)
746
            )->set(
747
                $this->handler->quoteColumn('sort_field'),
748
                $query->bindValue($location->sortField = $createStruct->sortField, null, \PDO::PARAM_INT)
749
            )->set(
750
                $this->handler->quoteColumn('sort_order'),
751
                $query->bindValue($location->sortOrder = $createStruct->sortOrder, null, \PDO::PARAM_INT)
752
            );
753
        $query->prepare()->execute();
754
755
        $location->id = $this->handler->lastInsertId($this->handler->getSequenceName('ezcontentobject_tree', 'node_id'));
756
757
        $mainLocationId = $createStruct->mainLocationId === true ? $location->id : $createStruct->mainLocationId;
758
        $location->pathString = $parentNode['path_string'] . $location->id . '/';
759
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
760
        $query = $this->handler->createUpdateQuery();
761
        $query
762
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
763
            ->set(
764
                $this->handler->quoteColumn('path_string'),
765
                $query->bindValue($location->pathString)
766
            )
767
            ->set(
768
                $this->handler->quoteColumn('main_node_id'),
769
                $query->bindValue($mainLocationId, null, \PDO::PARAM_INT)
770
            )
771
            ->where(
772
                $query->expr->eq(
773
                    $this->handler->quoteColumn('node_id'),
774
                    $query->bindValue($location->id, null, \PDO::PARAM_INT)
775
                )
776
            );
777
        $query->prepare()->execute();
778
779
        return $location;
780
    }
781
782
    /**
783
     * Create an entry in the node assignment table.
784
     *
785
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
786
     * @param mixed $parentNodeId
787
     * @param int $type
788
     */
789
    public function createNodeAssignment(CreateStruct $createStruct, $parentNodeId, $type = self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP)
790
    {
791
        $isMain = ($createStruct->mainLocationId === true ? 1 : 0);
792
793
        $query = $this->handler->createInsertQuery();
794
        $query
795
            ->insertInto($this->handler->quoteTable('eznode_assignment'))
796
            ->set(
797
                $this->handler->quoteColumn('contentobject_id'),
798
                $query->bindValue($createStruct->contentId, null, \PDO::PARAM_INT)
799
            )->set(
800
                $this->handler->quoteColumn('contentobject_version'),
801
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
802
            )->set(
803
                $this->handler->quoteColumn('from_node_id'),
804
                $query->bindValue(0, null, \PDO::PARAM_INT) // unused field
805
            )->set(
806
                $this->handler->quoteColumn('id'),
807
                $this->handler->getAutoIncrementValue('eznode_assignment', 'id')
808
            )->set(
809
                $this->handler->quoteColumn('is_main'),
810
                $query->bindValue($isMain, null, \PDO::PARAM_INT) // Changed by the business layer, later
811
            )->set(
812
                $this->handler->quoteColumn('op_code'),
813
                $query->bindValue($type, null, \PDO::PARAM_INT)
814
            )->set(
815
                $this->handler->quoteColumn('parent_node'),
816
                $query->bindValue($parentNodeId, null, \PDO::PARAM_INT)
817
            )->set(
818
                // parent_remote_id column should contain the remote id of the corresponding Location
819
                $this->handler->quoteColumn('parent_remote_id'),
820
                $query->bindValue($createStruct->remoteId, null, \PDO::PARAM_STR)
821
            )->set(
822
                // remote_id column should contain the remote id of the node assignment itself,
823
                // however this was never implemented completely in Legacy Stack, so we just set
824
                // it to default value '0'
825
                $this->handler->quoteColumn('remote_id'),
826
                $query->bindValue('0', null, \PDO::PARAM_STR)
827
            )->set(
828
                $this->handler->quoteColumn('sort_field'),
829
                $query->bindValue($createStruct->sortField, null, \PDO::PARAM_INT)
830
            )->set(
831
                $this->handler->quoteColumn('sort_order'),
832
                $query->bindValue($createStruct->sortOrder, null, \PDO::PARAM_INT)
833
            )->set(
834
                $this->handler->quoteColumn('priority'),
835
                $query->bindValue($createStruct->priority, null, \PDO::PARAM_INT)
836
            )->set(
837
                $this->handler->quoteColumn('is_hidden'),
838
                $query->bindValue($createStruct->hidden, null, \PDO::PARAM_INT)
839
            );
840
        $query->prepare()->execute();
841
    }
842
843
    /**
844
     * Deletes node assignment for given $contentId and $versionNo.
845
     *
846
     * If $versionNo is not passed all node assignments for given $contentId are deleted
847
     *
848
     * @param int $contentId
849
     * @param int|null $versionNo
850
     */
851
    public function deleteNodeAssignment($contentId, $versionNo = null)
852
    {
853
        $query = $this->handler->createDeleteQuery();
854
        $query->deleteFrom(
855
            'eznode_assignment'
856
        )->where(
857
            $query->expr->eq(
858
                $this->handler->quoteColumn('contentobject_id'),
859
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
860
            )
861
        );
862
        if (isset($versionNo)) {
863
            $query->where(
864
                $query->expr->eq(
865
                    $this->handler->quoteColumn('contentobject_version'),
866
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
867
                )
868
            );
869
        }
870
        $query->prepare()->execute();
871
    }
872
873
    /**
874
     * Update node assignment table.
875
     *
876
     * @param int $contentObjectId
877
     * @param int $oldParent
878
     * @param int $newParent
879
     * @param int $opcode
880
     */
881
    public function updateNodeAssignment($contentObjectId, $oldParent, $newParent, $opcode)
882
    {
883
        $query = $this->handler->createUpdateQuery();
884
        $query
885
            ->update($this->handler->quoteTable('eznode_assignment'))
886
            ->set(
887
                $this->handler->quoteColumn('parent_node'),
888
                $query->bindValue($newParent, null, \PDO::PARAM_INT)
889
            )
890
            ->set(
891
                $this->handler->quoteColumn('op_code'),
892
                $query->bindValue($opcode, null, \PDO::PARAM_INT)
893
            )
894
            ->where(
895
                $query->expr->lAnd(
896
                    $query->expr->eq(
897
                        $this->handler->quoteColumn('contentobject_id'),
898
                        $query->bindValue($contentObjectId, null, \PDO::PARAM_INT)
899
                    ),
900
                    $query->expr->eq(
901
                        $this->handler->quoteColumn('parent_node'),
902
                        $query->bindValue($oldParent, null, \PDO::PARAM_INT)
903
                    )
904
                )
905
            );
906
        $query->prepare()->execute();
907
    }
908
909
    /**
910
     * Create locations from node assignments.
911
     *
912
     * Convert existing node assignments into real locations.
913
     *
914
     * @param mixed $contentId
915
     * @param mixed $versionNo
916
     */
917
    public function createLocationsFromNodeAssignments($contentId, $versionNo)
918
    {
919
        // select all node assignments with OP_CODE_CREATE (3) for this content
920
        $query = $this->handler->createSelectQuery();
921
        $query
922
            ->select('*')
923
            ->from($this->handler->quoteTable('eznode_assignment'))
924
            ->where(
925
                $query->expr->lAnd(
926
                    $query->expr->eq(
927
                        $this->handler->quoteColumn('contentobject_id'),
928
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
929
                    ),
930
                    $query->expr->eq(
931
                        $this->handler->quoteColumn('contentobject_version'),
932
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
933
                    ),
934
                    $query->expr->eq(
935
                        $this->handler->quoteColumn('op_code'),
936
                        $query->bindValue(self::NODE_ASSIGNMENT_OP_CODE_CREATE, null, \PDO::PARAM_INT)
937
                    )
938
                )
939
            )->orderBy('id');
940
        $statement = $query->prepare();
941
        $statement->execute();
942
943
        // convert all these assignments to nodes
944
945
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
946
            if ((bool)$row['is_main'] === true) {
947
                $mainLocationId = true;
948
            } else {
949
                $mainLocationId = $this->getMainNodeId($contentId);
950
            }
951
952
            $parentLocationData = $this->getBasicNodeData($row['parent_node']);
953
            $isInvisible = $row['is_hidden'] || $parentLocationData['is_hidden'] || $parentLocationData['is_invisible'];
954
            $this->create(
955
                new CreateStruct(
956
                    array(
957
                        'contentId' => $row['contentobject_id'],
958
                        'contentVersion' => $row['contentobject_version'],
959
                        'mainLocationId' => $mainLocationId,
960
                        'remoteId' => $row['parent_remote_id'],
961
                        'sortField' => $row['sort_field'],
962
                        'sortOrder' => $row['sort_order'],
963
                        'priority' => $row['priority'],
964
                        'hidden' => $row['is_hidden'],
965
                        'invisible' => $isInvisible,
966
                    )
967
                ),
968
                $parentLocationData
969
            );
970
971
            $this->updateNodeAssignment(
972
                $row['contentobject_id'],
973
                $row['parent_node'],
974
                $row['parent_node'],
975
                self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP
976
            );
977
        }
978
    }
979
980
    /**
981
     * Updates all Locations of content identified with $contentId with $versionNo.
982
     *
983
     * @param mixed $contentId
984
     * @param mixed $versionNo
985
     */
986 View Code Duplication
    public function updateLocationsContentVersionNo($contentId, $versionNo)
987
    {
988
        $query = $this->handler->createUpdateQuery();
989
        $query->update(
990
            $this->handler->quoteTable('ezcontentobject_tree')
991
        )->set(
992
            $this->handler->quoteColumn('contentobject_version'),
993
            $query->bindValue($versionNo, null, \PDO::PARAM_INT)
994
        )->where(
995
            $query->expr->eq(
996
                $this->handler->quoteColumn('contentobject_id'),
997
                $contentId
998
            )
999
        );
1000
        $query->prepare()->execute();
1001
    }
1002
1003
    /**
1004
     * Searches for the main nodeId of $contentId in $versionId.
1005
     *
1006
     * @param int $contentId
1007
     *
1008
     * @return int|bool
1009
     */
1010
    private function getMainNodeId($contentId)
1011
    {
1012
        $query = $this->handler->createSelectQuery();
1013
        $query
1014
            ->select('node_id')
1015
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
1016
            ->where(
1017
                $query->expr->lAnd(
1018
                    $query->expr->eq(
1019
                        $this->handler->quoteColumn('contentobject_id'),
1020
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1021
                    ),
1022
                    $query->expr->eq(
1023
                        $this->handler->quoteColumn('node_id'),
1024
                        $this->handler->quoteColumn('main_node_id')
1025
                    )
1026
                )
1027
            );
1028
        $statement = $query->prepare();
1029
        $statement->execute();
1030
1031
        $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
1032
        if (count($result) === 1) {
1033
            return (int)$result[0]['node_id'];
1034
        } else {
1035
            return false;
1036
        }
1037
    }
1038
1039
    /**
1040
     * Updates an existing location.
1041
     *
1042
     * Will not throw anything if location id is invalid or no entries are affected.
1043
     *
1044
     * @param \eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct $location
1045
     * @param int $locationId
1046
     */
1047
    public function update(UpdateStruct $location, $locationId)
1048
    {
1049
        $query = $this->handler->createUpdateQuery();
1050
1051
        $query
1052
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
1053
            ->set(
1054
                $this->handler->quoteColumn('priority'),
1055
                $query->bindValue($location->priority)
1056
            )
1057
            ->set(
1058
                $this->handler->quoteColumn('remote_id'),
1059
                $query->bindValue($location->remoteId)
1060
            )
1061
            ->set(
1062
                $this->handler->quoteColumn('sort_order'),
1063
                $query->bindValue($location->sortOrder)
1064
            )
1065
            ->set(
1066
                $this->handler->quoteColumn('sort_field'),
1067
                $query->bindValue($location->sortField)
1068
            )
1069
            ->where(
1070
                $query->expr->eq(
1071
                    $this->handler->quoteColumn('node_id'),
1072
                    $locationId
1073
                )
1074
            );
1075
        $statement = $query->prepare();
1076
        $statement->execute();
1077
1078
        // Commented due to EZP-23302: Update Location fails if no change is performed with the update
1079
        // Should be fixed with PDO::MYSQL_ATTR_FOUND_ROWS instead
1080
        /*if ( $statement->rowCount() < 1 )
1081
        {
1082
            throw new NotFound( 'location', $locationId );
1083
        }*/
1084
    }
1085
1086
    /**
1087
     * Updates path identification string for given $locationId.
1088
     *
1089
     * @param mixed $locationId
1090
     * @param mixed $parentLocationId
1091
     * @param string $text
1092
     */
1093
    public function updatePathIdentificationString($locationId, $parentLocationId, $text)
1094
    {
1095
        $parentData = $this->getBasicNodeData($parentLocationId);
1096
1097
        $newPathIdentificationString = empty($parentData['path_identification_string']) ?
1098
            $text :
1099
            $parentData['path_identification_string'] . '/' . $text;
1100
1101
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1102
        $query = $this->handler->createUpdateQuery();
1103
        $query->update(
1104
            'ezcontentobject_tree'
1105
        )->set(
1106
            $this->handler->quoteColumn('path_identification_string'),
1107
            $query->bindValue($newPathIdentificationString, null, \PDO::PARAM_STR)
1108
        )->where(
1109
            $query->expr->eq(
1110
                $this->handler->quoteColumn('node_id'),
1111
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1112
            )
1113
        );
1114
        $query->prepare()->execute();
1115
    }
1116
1117
    /**
1118
     * Deletes ezcontentobject_tree row for given $locationId (node_id).
1119
     *
1120
     * @param mixed $locationId
1121
     */
1122
    public function removeLocation($locationId)
1123
    {
1124
        $query = $this->handler->createDeleteQuery();
1125
        $query->deleteFrom(
1126
            'ezcontentobject_tree'
1127
        )->where(
1128
            $query->expr->eq(
1129
                $this->handler->quoteColumn('node_id'),
1130
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1131
            )
1132
        );
1133
        $query->prepare()->execute();
1134
    }
1135
1136
    /**
1137
     * Returns id of the next in line node to be set as a new main node.
1138
     *
1139
     * This returns lowest node id for content identified by $contentId, and not of
1140
     * the node identified by given $locationId (current main node).
1141
     * Assumes that content has more than one location.
1142
     *
1143
     * @param mixed $contentId
1144
     * @param mixed $locationId
1145
     *
1146
     * @return array
1147
     */
1148
    public function getFallbackMainNodeData($contentId, $locationId)
1149
    {
1150
        $query = $this->handler->createSelectQuery();
1151
        $query->select(
1152
            $this->handler->quoteColumn('node_id'),
1153
            $this->handler->quoteColumn('contentobject_version'),
1154
            $this->handler->quoteColumn('parent_node_id')
1155
        )->from(
1156
            $this->handler->quoteTable('ezcontentobject_tree')
1157
        )->where(
1158
            $query->expr->lAnd(
1159
                $query->expr->eq(
1160
                    $this->handler->quoteColumn('contentobject_id'),
1161
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1162
                ),
1163
                $query->expr->neq(
1164
                    $this->handler->quoteColumn('node_id'),
1165
                    $query->bindValue($locationId, null, \PDO::PARAM_INT)
1166
                )
1167
            )
1168
        )->orderBy('node_id', SelectQuery::ASC)->limit(1);
1169
        $statement = $query->prepare();
1170
        $statement->execute();
1171
1172
        return $statement->fetch(\PDO::FETCH_ASSOC);
1173
    }
1174
1175
    /**
1176
     * Sends a single location identified by given $locationId to the trash.
1177
     *
1178
     * The associated content object is left untouched.
1179
     *
1180
     * @param mixed $locationId
1181
     *
1182
     * @return bool
1183
     */
1184
    public function trashLocation($locationId)
1185
    {
1186
        $locationRow = $this->getBasicNodeData($locationId);
1187
1188
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
1189
        $query = $this->handler->createInsertQuery();
1190
        $query->insertInto($this->handler->quoteTable('ezcontentobject_trash'));
1191
1192
        unset($locationRow['contentobject_is_published']);
1193
        $locationRow['trashed'] = time();
1194
        foreach ($locationRow as $key => $value) {
1195
            $query->set($key, $query->bindValue($value));
1196
        }
1197
1198
        $query->prepare()->execute();
1199
1200
        $this->removeLocation($locationRow['node_id']);
1201
        $this->setContentStatus($locationRow['contentobject_id'], ContentInfo::STATUS_TRASHED);
1202
    }
1203
1204
    /**
1205
     * Returns a trashed location to normal state.
1206
     *
1207
     * Recreates the originally trashed location in the new position. If no new
1208
     * position has been specified, it will be tried to re-create the location
1209
     * at the old position. If this is not possible ( because the old location
1210
     * does not exist any more) and exception is thrown.
1211
     *
1212
     * @param mixed $locationId
1213
     * @param mixed|null $newParentId
1214
     *
1215
     * @return \eZ\Publish\SPI\Persistence\Content\Location
1216
     */
1217
    public function untrashLocation($locationId, $newParentId = null)
1218
    {
1219
        $row = $this->loadTrashByLocation($locationId);
1220
1221
        $newLocation = $this->create(
1222
            new CreateStruct(
1223
                array(
1224
                    'priority' => $row['priority'],
1225
                    'hidden' => $row['is_hidden'],
1226
                    'invisible' => $row['is_invisible'],
1227
                    'remoteId' => $row['remote_id'],
1228
                    'contentId' => $row['contentobject_id'],
1229
                    'contentVersion' => $row['contentobject_version'],
1230
                    'mainLocationId' => true, // Restored location is always main location
1231
                    'sortField' => $row['sort_field'],
1232
                    'sortOrder' => $row['sort_order'],
1233
                )
1234
            ),
1235
            $this->getBasicNodeData($newParentId ?: $row['parent_node_id'])
1236
        );
1237
1238
        $this->removeElementFromTrash($locationId);
1239
        $this->setContentStatus($row['contentobject_id'], ContentInfo::STATUS_PUBLISHED);
1240
1241
        return $newLocation;
1242
    }
1243
1244
    /**
1245
     * @param mixed $contentId
1246
     * @param int $status
1247
     */
1248
    protected function setContentStatus($contentId, $status)
1249
    {
1250
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1251
        $query = $this->handler->createUpdateQuery();
1252
        $query->update(
1253
            'ezcontentobject'
1254
        )->set(
1255
            $this->handler->quoteColumn('status'),
1256
            $query->bindValue($status, null, \PDO::PARAM_INT)
1257
        )->where(
1258
            $query->expr->eq(
1259
                $this->handler->quoteColumn('id'),
1260
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1261
            )
1262
        );
1263
        $query->prepare()->execute();
1264
    }
1265
1266
    /**
1267
     * Loads trash data specified by location ID.
1268
     *
1269
     * @param mixed $locationId
1270
     *
1271
     * @return array
1272
     */
1273
    public function loadTrashByLocation($locationId)
1274
    {
1275
        $query = $this->handler->createSelectQuery();
1276
        $query
1277
            ->select('*')
1278
            ->from($this->handler->quoteTable('ezcontentobject_trash'))
1279
            ->where(
1280
                $query->expr->eq(
1281
                    $this->handler->quoteColumn('node_id'),
1282
                    $query->bindValue($locationId)
1283
                )
1284
            );
1285
        $statement = $query->prepare();
1286
        $statement->execute();
1287
1288
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1289
            return $row;
1290
        }
1291
1292
        throw new NotFound('trash', $locationId);
1293
    }
1294
1295
    /**
1296
     * List trashed items.
1297
     *
1298
     * @param int $offset
1299
     * @param int $limit
1300
     * @param array $sort
1301
     *
1302
     * @return array
1303
     */
1304
    public function listTrashed($offset, $limit, array $sort = null)
1305
    {
1306
        $query = $this->handler->createSelectQuery();
1307
        $query
1308
            ->select('*')
1309
            ->from($this->handler->quoteTable('ezcontentobject_trash'));
1310
1311
        $sort = $sort ?: array();
1312
        foreach ($sort as $condition) {
1313
            $sortDirection = $condition->direction === Query::SORT_ASC ? SelectQuery::ASC : SelectQuery::DESC;
1314
            switch (true) {
1315
                case $condition instanceof SortClause\Location\Depth:
1316
                    $query->orderBy('depth', $sortDirection);
1317
                    break;
1318
1319
                case $condition instanceof SortClause\Location\Path:
1320
                    $query->orderBy('path_string', $sortDirection);
1321
                    break;
1322
1323
                case $condition instanceof SortClause\Location\Priority:
1324
                    $query->orderBy('priority', $sortDirection);
1325
                    break;
1326
1327
                default:
1328
                    // Only handle location related sort clauses. The others
1329
                    // require data aggregation which is not sensible here.
1330
                    // Since also criteria are yet ignored, because they are
1331
                    // simply not used yet in eZ Publish, we skip that for now.
1332
                    throw new RuntimeException('Unhandled sort clause: ' . get_class($condition));
1333
            }
1334
        }
1335
1336
        if ($limit !== null) {
1337
            $query->limit($limit, $offset);
1338
        }
1339
1340
        $statement = $query->prepare();
1341
        $statement->execute();
1342
1343
        $rows = array();
1344
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1345
            $rows[] = $row;
1346
        }
1347
1348
        return $rows;
1349
    }
1350
1351
    /**
1352
     * Removes every entries in the trash.
1353
     * Will NOT remove associated content objects nor attributes.
1354
     *
1355
     * Basically truncates ezcontentobject_trash table.
1356
     */
1357
    public function cleanupTrash()
1358
    {
1359
        $query = $this->handler->createDeleteQuery();
1360
        $query->deleteFrom('ezcontentobject_trash');
1361
        $query->prepare()->execute();
1362
    }
1363
1364
    /**
1365
     * Removes trashed element identified by $id from trash.
1366
     * Will NOT remove associated content object nor attributes.
1367
     *
1368
     * @param int $id The trashed location Id
1369
     */
1370
    public function removeElementFromTrash($id)
1371
    {
1372
        $query = $this->handler->createDeleteQuery();
1373
        $query
1374
            ->deleteFrom('ezcontentobject_trash')
1375
            ->where(
1376
                $query->expr->eq(
1377
                    $this->handler->quoteColumn('node_id'),
1378
                    $query->bindValue($id, null, \PDO::PARAM_INT)
1379
                )
1380
            );
1381
        $query->prepare()->execute();
1382
    }
1383
1384
    /**
1385
     * Set section on all content objects in the subtree.
1386
     *
1387
     * @param string $pathString
1388
     * @param int $sectionId
1389
     *
1390
     * @return bool
1391
     */
1392
    public function setSectionForSubtree($pathString, $sectionId)
1393
    {
1394
        $selectContentIdsQuery = $this->connection->createQueryBuilder();
1395
        $selectContentIdsQuery
1396
            ->select('t.contentobject_id')
1397
            ->from('ezcontentobject_tree', 't')
1398
            ->where(
1399
                $selectContentIdsQuery->expr()->like(
1400
                    't.path_string',
1401
                    $selectContentIdsQuery->createPositionalParameter("{$pathString}%")
1402
                )
1403
            );
1404
1405
        $contentIds = array_map(
1406
            'intval',
1407
            $selectContentIdsQuery->execute()->fetchAll(PDO::FETCH_COLUMN)
1408
        );
1409
1410
        if (empty($contentIds)) {
1411
            return false;
1412
        }
1413
1414
        $updateSectionQuery = $this->connection->createQueryBuilder();
1415
        $updateSectionQuery
1416
            ->update('ezcontentobject')
1417
            ->set(
1418
                'section_id',
1419
                $updateSectionQuery->createPositionalParameter($sectionId, PDO::PARAM_INT)
1420
            )
1421
            ->where(
1422
                $updateSectionQuery->expr()->in(
1423
                    'id',
1424
                    $contentIds
1425
                )
1426
            );
1427
        $affectedRows = $updateSectionQuery->execute();
1428
1429
        return $affectedRows > 0;
1430
    }
1431
1432
    /**
1433
     * Returns how many locations given content object identified by $contentId has.
1434
     *
1435
     * @param int $contentId
1436
     *
1437
     * @return int
1438
     */
1439
    public function countLocationsByContentId($contentId)
1440
    {
1441
        $q = $this->handler->createSelectQuery();
1442
        $q
1443
            ->select(
1444
                $q->alias($q->expr->count('*'), 'count')
1445
            )
1446
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
1447
            ->where(
1448
                $q->expr->eq(
1449
                    $this->handler->quoteColumn('contentobject_id'),
1450
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1451
                )
1452
            );
1453
        $stmt = $q->prepare();
1454
        $stmt->execute();
1455
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1456
1457
        return (int)$res[0]['count'];
1458
    }
1459
1460
    /**
1461
     * Changes main location of content identified by given $contentId to location identified by given $locationId.
1462
     *
1463
     * Updates ezcontentobject_tree table for the given $contentId and eznode_assignment table for the given
1464
     * $contentId, $parentLocationId and $versionNo
1465
     *
1466
     * @param mixed $contentId
1467
     * @param mixed $locationId
1468
     * @param mixed $versionNo version number, needed to update eznode_assignment table
1469
     * @param mixed $parentLocationId parent location of location identified by $locationId, needed to update
1470
     *        eznode_assignment table
1471
     */
1472
    public function changeMainLocation($contentId, $locationId, $versionNo, $parentLocationId)
1473
    {
1474
        // Update ezcontentobject_tree table
1475
        $q = $this->handler->createUpdateQuery();
1476
        $q->update(
1477
            $this->handler->quoteTable('ezcontentobject_tree')
1478
        )->set(
1479
            $this->handler->quoteColumn('main_node_id'),
1480
            $q->bindValue($locationId, null, \PDO::PARAM_INT)
1481
        )->where(
1482
            $q->expr->eq(
1483
                $this->handler->quoteColumn('contentobject_id'),
1484
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
1485
            )
1486
        );
1487
        $q->prepare()->execute();
1488
1489
        // Erase is_main in eznode_assignment table
1490
        $q = $this->handler->createUpdateQuery();
1491
        $q->update(
1492
            $this->handler->quoteTable('eznode_assignment')
1493
        )->set(
1494
            $this->handler->quoteColumn('is_main'),
1495
            $q->bindValue(0, null, \PDO::PARAM_INT)
1496
        )->where(
1497
            $q->expr->lAnd(
1498
                $q->expr->eq(
1499
                    $this->handler->quoteColumn('contentobject_id'),
1500
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1501
                ),
1502
                $q->expr->eq(
1503
                    $this->handler->quoteColumn('contentobject_version'),
1504
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1505
                ),
1506
                $q->expr->neq(
1507
                    $this->handler->quoteColumn('parent_node'),
1508
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1509
                )
1510
            )
1511
        );
1512
        $q->prepare()->execute();
1513
1514
        // Set new is_main in eznode_assignment table
1515
        $q = $this->handler->createUpdateQuery();
1516
        $q->update(
1517
            $this->handler->quoteTable('eznode_assignment')
1518
        )->set(
1519
            $this->handler->quoteColumn('is_main'),
1520
            $q->bindValue(1, null, \PDO::PARAM_INT)
1521
        )->where(
1522
            $q->expr->lAnd(
1523
                $q->expr->eq(
1524
                    $this->handler->quoteColumn('contentobject_id'),
1525
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1526
                ),
1527
                $q->expr->eq(
1528
                    $this->handler->quoteColumn('contentobject_version'),
1529
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1530
                ),
1531
                $q->expr->eq(
1532
                    $this->handler->quoteColumn('parent_node'),
1533
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1534
                )
1535
            )
1536
        );
1537
        $q->prepare()->execute();
1538
    }
1539
1540
    /**
1541
     * Get the total number of all Locations, except the Root node.
1542
     *
1543
     * @see loadAllLocationsData
1544
     *
1545
     * @return int
1546
     */
1547
    public function countAllLocations()
1548
    {
1549
        $query = $this->getAllLocationsQueryBuilder(['count(node_id)']);
1550
1551
        $statement = $query->execute();
1552
1553
        return (int) $statement->fetch(PDO::FETCH_COLUMN);
1554
    }
1555
1556
    /**
1557
     * Load data of every Location, except the Root node.
1558
     *
1559
     * @param int $offset Paginator offset
1560
     * @param int $limit Paginator limit
1561
     *
1562
     * @return array
1563
     */
1564
    public function loadAllLocationsData($offset, $limit)
1565
    {
1566
        $query = $this
1567
            ->getAllLocationsQueryBuilder(
1568
                [
1569
                    'node_id',
1570
                    'priority',
1571
                    'is_hidden',
1572
                    'is_invisible',
1573
                    'remote_id',
1574
                    'contentobject_id',
1575
                    'parent_node_id',
1576
                    'path_identification_string',
1577
                    'path_string',
1578
                    'depth',
1579
                    'sort_field',
1580
                    'sort_order',
1581
                ]
1582
            )
1583
            ->setFirstResult($offset)
1584
            ->setMaxResults($limit)
1585
            ->orderBy('depth', 'ASC')
1586
            ->addOrderBy('node_id', 'ASC')
1587
        ;
1588
1589
        $statement = $query->execute();
1590
1591
        return $statement->fetchAll(PDO::FETCH_ASSOC);
1592
    }
1593
1594
    /**
1595
     * Get Query Builder for fetching data of all Locations except the Root node.
1596
     *
1597
     * @todo Align with createNodeQueryBuilder, removing the need for both(?)
1598
     *
1599
     * @param array $columns list of columns to fetch
1600
     *
1601
     * @return \Doctrine\DBAL\Query\QueryBuilder
1602
     */
1603
    private function getAllLocationsQueryBuilder(array $columns)
1604
    {
1605
        $query = $this->connection->createQueryBuilder();
1606
        $query
1607
            ->select($columns)
1608
            ->from('ezcontentobject_tree')
1609
            ->where($query->expr()->neq('node_id', 'parent_node_id'))
1610
        ;
1611
1612
        return $query;
1613
    }
1614
1615
    /**
1616
     * Create QueryBuilder for selecting node data.
1617
     *
1618
     * @param array|null $translations Filters on language mask of content if provided.
1619
     * @param bool $useAlwaysAvailable Respect always available flag on content when filtering on $translations.
1620
     *
1621
     * @return \Doctrine\DBAL\Query\QueryBuilder
1622
     */
1623
    private function createNodeQueryBuilder(array $translations = null, bool $useAlwaysAvailable = true): QueryBuilder
1624
    {
1625
        $queryBuilder = $this->connection->createQueryBuilder();
1626
        $queryBuilder
1627
            ->select('t.*')
1628
            ->from('ezcontentobject_tree', 't');
1629
1630
        if (!empty($translations)) {
1631
            $dbPlatform = $this->connection->getDatabasePlatform();
1632
            $expr = $queryBuilder->expr();
1633
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
1634
                $translations,
1635
                $useAlwaysAvailable
1636
            );
1637
1638
            $queryBuilder->innerJoin(
1639
                't',
1640
                'ezcontentobject',
1641
                'c',
1642
                $expr->andX(
1643
                    $expr->eq('t.contentobject_id', 'c.id'),
1644
                    $expr->gt(
1645
                        $dbPlatform->getBitAndComparisonExpression('c.language_mask', $mask),
1646
                        0
1647
                    )
1648
                )
1649
            );
1650
        }
1651
1652
        return $queryBuilder;
1653
    }
1654
}
1655