Completed
Push — master ( 08b605...3e0b85 )
by Łukasz
22:32
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
        $query = $this->handler->createUpdateQuery();
425
        $query
426
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
427
            ->set(
428
                $this->handler->quoteColumn('is_invisible'),
429
                $query->bindValue(1)
430
            )
431
            ->set(
432
                $this->handler->quoteColumn('modified_subnode'),
433
                $query->bindValue(time())
434
            )
435
            ->where(
436
                $query->expr->like(
437
                    $this->handler->quoteColumn('path_string'),
438
                    $query->bindValue($pathString . '%')
439
                )
440
            );
441
        $query->prepare()->execute();
442
443
        $query = $this->handler->createUpdateQuery();
444
        $query
445
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
446
            ->set(
447
                $this->handler->quoteColumn('is_hidden'),
448
                $query->bindValue(1)
449
            )
450
            ->where(
451
                $query->expr->eq(
452
                    $this->handler->quoteColumn('path_string'),
453
                    $query->bindValue($pathString)
454
                )
455
            );
456
        $query->prepare()->execute();
457
    }
458
459
    /**
460
     * Sets a location to be unhidden, and self + children to visible unless a parent is hiding the tree.
461
     * If not make sure only children down to first hidden node is marked visible.
462
     *
463
     * @param string $pathString
464
     */
465
    public function unHideSubtree($pathString)
466
    {
467
        // Unhide the requested node
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(0)
474
            )
475
            ->where(
476
                $query->expr->eq(
477
                    $this->handler->quoteColumn('path_string'),
478
                    $query->bindValue($pathString)
479
                )
480
            );
481
        $query->prepare()->execute();
482
483
        // Check if any parent nodes are explicitly hidden
484
        $query = $this->handler->createSelectQuery();
485
        $query
486
            ->select($this->handler->quoteColumn('path_string'))
487
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
488
            ->where(
489
                $query->expr->lAnd(
490
                    $query->expr->eq(
491
                        $this->handler->quoteColumn('is_hidden'),
492
                        $query->bindValue(1)
493
                    ),
494
                    $query->expr->in(
495
                        $this->handler->quoteColumn('node_id'),
496
                        array_filter(explode('/', $pathString))
497
                    )
498
                )
499
            );
500
        $statement = $query->prepare();
501
        $statement->execute();
502
        if (count($statement->fetchAll(\PDO::FETCH_COLUMN))) {
503
            // There are parent nodes set hidden, so that we can skip marking
504
            // something visible again.
505
            return;
506
        }
507
508
        // Find nodes of explicitly hidden subtrees in the subtree which
509
        // should be unhidden
510
        $query = $this->handler->createSelectQuery();
511
        $query
512
            ->select($this->handler->quoteColumn('path_string'))
513
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
514
            ->where(
515
                $query->expr->lAnd(
516
                    $query->expr->eq(
517
                        $this->handler->quoteColumn('is_hidden'),
518
                        $query->bindValue(1)
519
                    ),
520
                    $query->expr->like(
521
                        $this->handler->quoteColumn('path_string'),
522
                        $query->bindValue($pathString . '%')
523
                    )
524
                )
525
            );
526
        $statement = $query->prepare();
527
        $statement->execute();
528
        $hiddenSubtrees = $statement->fetchAll(\PDO::FETCH_COLUMN);
529
530
        $query = $this->handler->createUpdateQuery();
531
        $query
532
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
533
            ->set(
534
                $this->handler->quoteColumn('is_invisible'),
535
                $query->bindValue(0)
536
            )
537
            ->set(
538
                $this->handler->quoteColumn('modified_subnode'),
539
                $query->bindValue(time())
540
            );
541
542
        // Build where expression selecting the nodes, which should be made
543
        // visible again
544
        $where = $query->expr->like(
545
            $this->handler->quoteColumn('path_string'),
546
            $query->bindValue($pathString . '%')
547
        );
548
        if (count($hiddenSubtrees)) {
549
            $handler = $this->handler;
550
            $where = $query->expr->lAnd(
551
                $where,
552
                $query->expr->lAnd(
553
                    array_map(
554
                        function ($pathString) use ($query, $handler) {
555
                            return $query->expr->not(
556
                                $query->expr->like(
557
                                    $handler->quoteColumn('path_string'),
558
                                    $query->bindValue($pathString . '%')
559
                                )
560
                            );
561
                        },
562
                        $hiddenSubtrees
563
                    )
564
                )
565
            );
566
        }
567
        $query->where($where);
568
        $statement = $query->prepare()->execute();
0 ignored issues
show
Unused Code introduced by
$statement is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
569
    }
570
571
    /**
572
     * Swaps the content object being pointed to by a location object.
573
     *
574
     * Make the location identified by $locationId1 refer to the Content
575
     * referred to by $locationId2 and vice versa.
576
     *
577
     * @param mixed $locationId1
578
     * @param mixed $locationId2
579
     *
580
     * @return bool
581
     */
582
    public function swap($locationId1, $locationId2)
583
    {
584
        $query = $this->handler->createSelectQuery();
585
        $query
586
            ->select(
587
                $this->handler->quoteColumn('node_id'),
588
                $this->handler->quoteColumn('contentobject_id'),
589
                $this->handler->quoteColumn('contentobject_version')
590
            )
591
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
592
            ->where(
593
                $query->expr->in(
594
                    $this->handler->quoteColumn('node_id'),
595
                    array($locationId1, $locationId2)
596
                )
597
            );
598
        $statement = $query->prepare();
599
        $statement->execute();
600
        foreach ($statement->fetchAll() as $row) {
601
            $contentObjects[$row['node_id']] = $row;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$contentObjects was never initialized. Although not strictly required by PHP, it is generally a good practice to add $contentObjects = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
602
        }
603
604
        $query = $this->handler->createUpdateQuery();
605
        $query
606
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
607
            ->set(
608
                $this->handler->quoteColumn('contentobject_id'),
609
                $query->bindValue($contentObjects[$locationId2]['contentobject_id'])
0 ignored issues
show
Bug introduced by
The variable $contentObjects does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
610
            )
611
            ->set(
612
                $this->handler->quoteColumn('contentobject_version'),
613
                $query->bindValue($contentObjects[$locationId2]['contentobject_version'])
614
            )
615
            ->where(
616
                $query->expr->eq(
617
                    $this->handler->quoteColumn('node_id'),
618
                    $query->bindValue($locationId1)
619
                )
620
            );
621
        $query->prepare()->execute();
622
623
        $query = $this->handler->createUpdateQuery();
624
        $query
625
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
626
            ->set(
627
                $this->handler->quoteColumn('contentobject_id'),
628
                $query->bindValue($contentObjects[$locationId1]['contentobject_id'])
629
            )
630
            ->set(
631
                $this->handler->quoteColumn('contentobject_version'),
632
                $query->bindValue($contentObjects[$locationId1]['contentobject_version'])
633
            )
634
            ->where(
635
                $query->expr->eq(
636
                    $this->handler->quoteColumn('node_id'),
637
                    $query->bindValue($locationId2)
638
                )
639
            );
640
        $query->prepare()->execute();
641
    }
642
643
    /**
644
     * Creates a new location in given $parentNode.
645
     *
646
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
647
     * @param array $parentNode
648
     *
649
     * @return \eZ\Publish\SPI\Persistence\Content\Location
650
     */
651
    public function create(CreateStruct $createStruct, array $parentNode)
652
    {
653
        $location = new Location();
654
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
655
        $query = $this->handler->createInsertQuery();
656
        $query
657
            ->insertInto($this->handler->quoteTable('ezcontentobject_tree'))
658
            ->set(
659
                $this->handler->quoteColumn('contentobject_id'),
660
                $query->bindValue($location->contentId = $createStruct->contentId, null, \PDO::PARAM_INT)
661
            )->set(
662
                $this->handler->quoteColumn('contentobject_is_published'),
663
                $query->bindValue(1, null, \PDO::PARAM_INT)
664
            )->set(
665
                $this->handler->quoteColumn('contentobject_version'),
666
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
667
            )->set(
668
                $this->handler->quoteColumn('depth'),
669
                $query->bindValue($location->depth = $parentNode['depth'] + 1, null, \PDO::PARAM_INT)
670
            )->set(
671
                $this->handler->quoteColumn('is_hidden'),
672
                $query->bindValue($location->hidden = $createStruct->hidden, null, \PDO::PARAM_INT)
673
            )->set(
674
                $this->handler->quoteColumn('is_invisible'),
675
                $query->bindValue($location->invisible = $createStruct->invisible, null, \PDO::PARAM_INT)
676
            )->set(
677
                $this->handler->quoteColumn('modified_subnode'),
678
                $query->bindValue(time(), null, \PDO::PARAM_INT)
679
            )->set(
680
                $this->handler->quoteColumn('node_id'),
681
                $this->handler->getAutoIncrementValue('ezcontentobject_tree', 'node_id')
682
            )->set(
683
                $this->handler->quoteColumn('parent_node_id'),
684
                $query->bindValue($location->parentId = $parentNode['node_id'], null, \PDO::PARAM_INT)
685
            )->set(
686
                $this->handler->quoteColumn('path_identification_string'),
687
                $query->bindValue($location->pathIdentificationString = $createStruct->pathIdentificationString, null, \PDO::PARAM_STR)
688
            )->set(
689
                $this->handler->quoteColumn('path_string'),
690
                $query->bindValue('dummy') // Set later
691
            )->set(
692
                $this->handler->quoteColumn('priority'),
693
                $query->bindValue($location->priority = $createStruct->priority, null, \PDO::PARAM_INT)
694
            )->set(
695
                $this->handler->quoteColumn('remote_id'),
696
                $query->bindValue($location->remoteId = $createStruct->remoteId, null, \PDO::PARAM_STR)
697
            )->set(
698
                $this->handler->quoteColumn('sort_field'),
699
                $query->bindValue($location->sortField = $createStruct->sortField, null, \PDO::PARAM_INT)
700
            )->set(
701
                $this->handler->quoteColumn('sort_order'),
702
                $query->bindValue($location->sortOrder = $createStruct->sortOrder, null, \PDO::PARAM_INT)
703
            );
704
        $query->prepare()->execute();
705
706
        $location->id = $this->handler->lastInsertId($this->handler->getSequenceName('ezcontentobject_tree', 'node_id'));
707
708
        $mainLocationId = $createStruct->mainLocationId === true ? $location->id : $createStruct->mainLocationId;
709
        $location->pathString = $parentNode['path_string'] . $location->id . '/';
710
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
711
        $query = $this->handler->createUpdateQuery();
712
        $query
713
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
714
            ->set(
715
                $this->handler->quoteColumn('path_string'),
716
                $query->bindValue($location->pathString)
717
            )
718
            ->set(
719
                $this->handler->quoteColumn('main_node_id'),
720
                $query->bindValue($mainLocationId, null, \PDO::PARAM_INT)
721
            )
722
            ->where(
723
                $query->expr->eq(
724
                    $this->handler->quoteColumn('node_id'),
725
                    $query->bindValue($location->id, null, \PDO::PARAM_INT)
726
                )
727
            );
728
        $query->prepare()->execute();
729
730
        return $location;
731
    }
732
733
    /**
734
     * Create an entry in the node assignment table.
735
     *
736
     * @param \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct $createStruct
737
     * @param mixed $parentNodeId
738
     * @param int $type
739
     */
740
    public function createNodeAssignment(CreateStruct $createStruct, $parentNodeId, $type = self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP)
741
    {
742
        $isMain = ($createStruct->mainLocationId === true ? 1 : 0);
743
744
        $query = $this->handler->createInsertQuery();
745
        $query
746
            ->insertInto($this->handler->quoteTable('eznode_assignment'))
747
            ->set(
748
                $this->handler->quoteColumn('contentobject_id'),
749
                $query->bindValue($createStruct->contentId, null, \PDO::PARAM_INT)
750
            )->set(
751
                $this->handler->quoteColumn('contentobject_version'),
752
                $query->bindValue($createStruct->contentVersion, null, \PDO::PARAM_INT)
753
            )->set(
754
                $this->handler->quoteColumn('from_node_id'),
755
                $query->bindValue(0, null, \PDO::PARAM_INT) // unused field
756
            )->set(
757
                $this->handler->quoteColumn('id'),
758
                $this->handler->getAutoIncrementValue('eznode_assignment', 'id')
759
            )->set(
760
                $this->handler->quoteColumn('is_main'),
761
                $query->bindValue($isMain, null, \PDO::PARAM_INT) // Changed by the business layer, later
762
            )->set(
763
                $this->handler->quoteColumn('op_code'),
764
                $query->bindValue($type, null, \PDO::PARAM_INT)
765
            )->set(
766
                $this->handler->quoteColumn('parent_node'),
767
                $query->bindValue($parentNodeId, null, \PDO::PARAM_INT)
768
            )->set(
769
                // parent_remote_id column should contain the remote id of the corresponding Location
770
                $this->handler->quoteColumn('parent_remote_id'),
771
                $query->bindValue($createStruct->remoteId, null, \PDO::PARAM_STR)
772
            )->set(
773
                // remote_id column should contain the remote id of the node assignment itself,
774
                // however this was never implemented completely in Legacy Stack, so we just set
775
                // it to default value '0'
776
                $this->handler->quoteColumn('remote_id'),
777
                $query->bindValue('0', null, \PDO::PARAM_STR)
778
            )->set(
779
                $this->handler->quoteColumn('sort_field'),
780
                $query->bindValue($createStruct->sortField, null, \PDO::PARAM_INT)
781
            )->set(
782
                $this->handler->quoteColumn('sort_order'),
783
                $query->bindValue($createStruct->sortOrder, null, \PDO::PARAM_INT)
784
            )->set(
785
                $this->handler->quoteColumn('priority'),
786
                $query->bindValue($createStruct->priority, null, \PDO::PARAM_INT)
787
            )->set(
788
                $this->handler->quoteColumn('is_hidden'),
789
                $query->bindValue($createStruct->hidden, null, \PDO::PARAM_INT)
790
            );
791
        $query->prepare()->execute();
792
    }
793
794
    /**
795
     * Deletes node assignment for given $contentId and $versionNo.
796
     *
797
     * If $versionNo is not passed all node assignments for given $contentId are deleted
798
     *
799
     * @param int $contentId
800
     * @param int|null $versionNo
801
     */
802
    public function deleteNodeAssignment($contentId, $versionNo = null)
803
    {
804
        $query = $this->handler->createDeleteQuery();
805
        $query->deleteFrom(
806
            'eznode_assignment'
807
        )->where(
808
            $query->expr->eq(
809
                $this->handler->quoteColumn('contentobject_id'),
810
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
811
            )
812
        );
813
        if (isset($versionNo)) {
814
            $query->where(
815
                $query->expr->eq(
816
                    $this->handler->quoteColumn('contentobject_version'),
817
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
818
                )
819
            );
820
        }
821
        $query->prepare()->execute();
822
    }
823
824
    /**
825
     * Update node assignment table.
826
     *
827
     * @param int $contentObjectId
828
     * @param int $oldParent
829
     * @param int $newParent
830
     * @param int $opcode
831
     */
832
    public function updateNodeAssignment($contentObjectId, $oldParent, $newParent, $opcode)
833
    {
834
        $query = $this->handler->createUpdateQuery();
835
        $query
836
            ->update($this->handler->quoteTable('eznode_assignment'))
837
            ->set(
838
                $this->handler->quoteColumn('parent_node'),
839
                $query->bindValue($newParent, null, \PDO::PARAM_INT)
840
            )
841
            ->set(
842
                $this->handler->quoteColumn('op_code'),
843
                $query->bindValue($opcode, null, \PDO::PARAM_INT)
844
            )
845
            ->where(
846
                $query->expr->lAnd(
847
                    $query->expr->eq(
848
                        $this->handler->quoteColumn('contentobject_id'),
849
                        $query->bindValue($contentObjectId, null, \PDO::PARAM_INT)
850
                    ),
851
                    $query->expr->eq(
852
                        $this->handler->quoteColumn('parent_node'),
853
                        $query->bindValue($oldParent, null, \PDO::PARAM_INT)
854
                    )
855
                )
856
            );
857
        $query->prepare()->execute();
858
    }
859
860
    /**
861
     * Create locations from node assignments.
862
     *
863
     * Convert existing node assignments into real locations.
864
     *
865
     * @param mixed $contentId
866
     * @param mixed $versionNo
867
     */
868
    public function createLocationsFromNodeAssignments($contentId, $versionNo)
869
    {
870
        // select all node assignments with OP_CODE_CREATE (3) for this content
871
        $query = $this->handler->createSelectQuery();
872
        $query
873
            ->select('*')
874
            ->from($this->handler->quoteTable('eznode_assignment'))
875
            ->where(
876
                $query->expr->lAnd(
877
                    $query->expr->eq(
878
                        $this->handler->quoteColumn('contentobject_id'),
879
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
880
                    ),
881
                    $query->expr->eq(
882
                        $this->handler->quoteColumn('contentobject_version'),
883
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
884
                    ),
885
                    $query->expr->eq(
886
                        $this->handler->quoteColumn('op_code'),
887
                        $query->bindValue(self::NODE_ASSIGNMENT_OP_CODE_CREATE, null, \PDO::PARAM_INT)
888
                    )
889
                )
890
            )->orderBy('id');
891
        $statement = $query->prepare();
892
        $statement->execute();
893
894
        // convert all these assignments to nodes
895
896
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
897
            if ((bool)$row['is_main'] === true) {
898
                $mainLocationId = true;
899
            } else {
900
                $mainLocationId = $this->getMainNodeId($contentId);
901
            }
902
903
            $parentLocationData = $this->getBasicNodeData($row['parent_node']);
904
            $isInvisible = $row['is_hidden'] || $parentLocationData['is_hidden'] || $parentLocationData['is_invisible'];
905
            $this->create(
906
                new CreateStruct(
907
                    array(
908
                        'contentId' => $row['contentobject_id'],
909
                        'contentVersion' => $row['contentobject_version'],
910
                        'mainLocationId' => $mainLocationId,
911
                        'remoteId' => $row['parent_remote_id'],
912
                        'sortField' => $row['sort_field'],
913
                        'sortOrder' => $row['sort_order'],
914
                        'priority' => $row['priority'],
915
                        'hidden' => $row['is_hidden'],
916
                        'invisible' => $isInvisible,
917
                    )
918
                ),
919
                $parentLocationData
920
            );
921
922
            $this->updateNodeAssignment(
923
                $row['contentobject_id'],
924
                $row['parent_node'],
925
                $row['parent_node'],
926
                self::NODE_ASSIGNMENT_OP_CODE_CREATE_NOP
927
            );
928
        }
929
    }
930
931
    /**
932
     * Updates all Locations of content identified with $contentId with $versionNo.
933
     *
934
     * @param mixed $contentId
935
     * @param mixed $versionNo
936
     */
937 View Code Duplication
    public function updateLocationsContentVersionNo($contentId, $versionNo)
938
    {
939
        $query = $this->handler->createUpdateQuery();
940
        $query->update(
941
            $this->handler->quoteTable('ezcontentobject_tree')
942
        )->set(
943
            $this->handler->quoteColumn('contentobject_version'),
944
            $query->bindValue($versionNo, null, \PDO::PARAM_INT)
945
        )->where(
946
            $query->expr->eq(
947
                $this->handler->quoteColumn('contentobject_id'),
948
                $contentId
949
            )
950
        );
951
        $query->prepare()->execute();
952
    }
953
954
    /**
955
     * Searches for the main nodeId of $contentId in $versionId.
956
     *
957
     * @param int $contentId
958
     *
959
     * @return int|bool
960
     */
961
    private function getMainNodeId($contentId)
962
    {
963
        $query = $this->handler->createSelectQuery();
964
        $query
965
            ->select('node_id')
966
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
967
            ->where(
968
                $query->expr->lAnd(
969
                    $query->expr->eq(
970
                        $this->handler->quoteColumn('contentobject_id'),
971
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
972
                    ),
973
                    $query->expr->eq(
974
                        $this->handler->quoteColumn('node_id'),
975
                        $this->handler->quoteColumn('main_node_id')
976
                    )
977
                )
978
            );
979
        $statement = $query->prepare();
980
        $statement->execute();
981
982
        $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
983
        if (count($result) === 1) {
984
            return (int)$result[0]['node_id'];
985
        } else {
986
            return false;
987
        }
988
    }
989
990
    /**
991
     * Updates an existing location.
992
     *
993
     * Will not throw anything if location id is invalid or no entries are affected.
994
     *
995
     * @param \eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct $location
996
     * @param int $locationId
997
     */
998
    public function update(UpdateStruct $location, $locationId)
999
    {
1000
        $query = $this->handler->createUpdateQuery();
1001
1002
        $query
1003
            ->update($this->handler->quoteTable('ezcontentobject_tree'))
1004
            ->set(
1005
                $this->handler->quoteColumn('priority'),
1006
                $query->bindValue($location->priority)
1007
            )
1008
            ->set(
1009
                $this->handler->quoteColumn('remote_id'),
1010
                $query->bindValue($location->remoteId)
1011
            )
1012
            ->set(
1013
                $this->handler->quoteColumn('sort_order'),
1014
                $query->bindValue($location->sortOrder)
1015
            )
1016
            ->set(
1017
                $this->handler->quoteColumn('sort_field'),
1018
                $query->bindValue($location->sortField)
1019
            )
1020
            ->where(
1021
                $query->expr->eq(
1022
                    $this->handler->quoteColumn('node_id'),
1023
                    $locationId
1024
                )
1025
            );
1026
        $statement = $query->prepare();
1027
        $statement->execute();
1028
1029
        // Commented due to EZP-23302: Update Location fails if no change is performed with the update
1030
        // Should be fixed with PDO::MYSQL_ATTR_FOUND_ROWS instead
1031
        /*if ( $statement->rowCount() < 1 )
1032
        {
1033
            throw new NotFound( 'location', $locationId );
1034
        }*/
1035
    }
1036
1037
    /**
1038
     * Updates path identification string for given $locationId.
1039
     *
1040
     * @param mixed $locationId
1041
     * @param mixed $parentLocationId
1042
     * @param string $text
1043
     */
1044
    public function updatePathIdentificationString($locationId, $parentLocationId, $text)
1045
    {
1046
        $parentData = $this->getBasicNodeData($parentLocationId);
1047
1048
        $newPathIdentificationString = empty($parentData['path_identification_string']) ?
1049
            $text :
1050
            $parentData['path_identification_string'] . '/' . $text;
1051
1052
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1053
        $query = $this->handler->createUpdateQuery();
1054
        $query->update(
1055
            'ezcontentobject_tree'
1056
        )->set(
1057
            $this->handler->quoteColumn('path_identification_string'),
1058
            $query->bindValue($newPathIdentificationString, null, \PDO::PARAM_STR)
1059
        )->where(
1060
            $query->expr->eq(
1061
                $this->handler->quoteColumn('node_id'),
1062
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1063
            )
1064
        );
1065
        $query->prepare()->execute();
1066
    }
1067
1068
    /**
1069
     * Deletes ezcontentobject_tree row for given $locationId (node_id).
1070
     *
1071
     * @param mixed $locationId
1072
     */
1073
    public function removeLocation($locationId)
1074
    {
1075
        $query = $this->handler->createDeleteQuery();
1076
        $query->deleteFrom(
1077
            'ezcontentobject_tree'
1078
        )->where(
1079
            $query->expr->eq(
1080
                $this->handler->quoteColumn('node_id'),
1081
                $query->bindValue($locationId, null, \PDO::PARAM_INT)
1082
            )
1083
        );
1084
        $query->prepare()->execute();
1085
    }
1086
1087
    /**
1088
     * Returns id of the next in line node to be set as a new main node.
1089
     *
1090
     * This returns lowest node id for content identified by $contentId, and not of
1091
     * the node identified by given $locationId (current main node).
1092
     * Assumes that content has more than one location.
1093
     *
1094
     * @param mixed $contentId
1095
     * @param mixed $locationId
1096
     *
1097
     * @return array
1098
     */
1099
    public function getFallbackMainNodeData($contentId, $locationId)
1100
    {
1101
        $query = $this->handler->createSelectQuery();
1102
        $query->select(
1103
            $this->handler->quoteColumn('node_id'),
1104
            $this->handler->quoteColumn('contentobject_version'),
1105
            $this->handler->quoteColumn('parent_node_id')
1106
        )->from(
1107
            $this->handler->quoteTable('ezcontentobject_tree')
1108
        )->where(
1109
            $query->expr->lAnd(
1110
                $query->expr->eq(
1111
                    $this->handler->quoteColumn('contentobject_id'),
1112
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1113
                ),
1114
                $query->expr->neq(
1115
                    $this->handler->quoteColumn('node_id'),
1116
                    $query->bindValue($locationId, null, \PDO::PARAM_INT)
1117
                )
1118
            )
1119
        )->orderBy('node_id', SelectQuery::ASC)->limit(1);
1120
        $statement = $query->prepare();
1121
        $statement->execute();
1122
1123
        return $statement->fetch(\PDO::FETCH_ASSOC);
1124
    }
1125
1126
    /**
1127
     * Sends a single location identified by given $locationId to the trash.
1128
     *
1129
     * The associated content object is left untouched.
1130
     *
1131
     * @param mixed $locationId
1132
     *
1133
     * @return bool
1134
     */
1135
    public function trashLocation($locationId)
1136
    {
1137
        $locationRow = $this->getBasicNodeData($locationId);
1138
1139
        /** @var $query \eZ\Publish\Core\Persistence\Database\InsertQuery */
1140
        $query = $this->handler->createInsertQuery();
1141
        $query->insertInto($this->handler->quoteTable('ezcontentobject_trash'));
1142
1143
        unset($locationRow['contentobject_is_published']);
1144
        $locationRow['trashed'] = time();
1145
        foreach ($locationRow as $key => $value) {
1146
            $query->set($key, $query->bindValue($value));
1147
        }
1148
1149
        $query->prepare()->execute();
1150
1151
        $this->removeLocation($locationRow['node_id']);
1152
        $this->setContentStatus($locationRow['contentobject_id'], ContentInfo::STATUS_TRASHED);
1153
    }
1154
1155
    /**
1156
     * Returns a trashed location to normal state.
1157
     *
1158
     * Recreates the originally trashed location in the new position. If no new
1159
     * position has been specified, it will be tried to re-create the location
1160
     * at the old position. If this is not possible ( because the old location
1161
     * does not exist any more) and exception is thrown.
1162
     *
1163
     * @param mixed $locationId
1164
     * @param mixed|null $newParentId
1165
     *
1166
     * @return \eZ\Publish\SPI\Persistence\Content\Location
1167
     */
1168
    public function untrashLocation($locationId, $newParentId = null)
1169
    {
1170
        $row = $this->loadTrashByLocation($locationId);
1171
1172
        $newLocation = $this->create(
1173
            new CreateStruct(
1174
                array(
1175
                    'priority' => $row['priority'],
1176
                    'hidden' => $row['is_hidden'],
1177
                    'invisible' => $row['is_invisible'],
1178
                    'remoteId' => $row['remote_id'],
1179
                    'contentId' => $row['contentobject_id'],
1180
                    'contentVersion' => $row['contentobject_version'],
1181
                    'mainLocationId' => true, // Restored location is always main location
1182
                    'sortField' => $row['sort_field'],
1183
                    'sortOrder' => $row['sort_order'],
1184
                )
1185
            ),
1186
            $this->getBasicNodeData($newParentId ?: $row['parent_node_id'])
1187
        );
1188
1189
        $this->removeElementFromTrash($locationId);
1190
        $this->setContentStatus($row['contentobject_id'], ContentInfo::STATUS_PUBLISHED);
1191
1192
        return $newLocation;
1193
    }
1194
1195
    /**
1196
     * @param mixed $contentId
1197
     * @param int $status
1198
     */
1199
    protected function setContentStatus($contentId, $status)
1200
    {
1201
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1202
        $query = $this->handler->createUpdateQuery();
1203
        $query->update(
1204
            'ezcontentobject'
1205
        )->set(
1206
            $this->handler->quoteColumn('status'),
1207
            $query->bindValue($status, null, \PDO::PARAM_INT)
1208
        )->where(
1209
            $query->expr->eq(
1210
                $this->handler->quoteColumn('id'),
1211
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1212
            )
1213
        );
1214
        $query->prepare()->execute();
1215
    }
1216
1217
    /**
1218
     * Loads trash data specified by location ID.
1219
     *
1220
     * @param mixed $locationId
1221
     *
1222
     * @return array
1223
     */
1224 View Code Duplication
    public function loadTrashByLocation($locationId)
1225
    {
1226
        $query = $this->handler->createSelectQuery();
1227
        $query
1228
            ->select('*')
1229
            ->from($this->handler->quoteTable('ezcontentobject_trash'))
1230
            ->where(
1231
                $query->expr->eq(
1232
                    $this->handler->quoteColumn('node_id'),
1233
                    $query->bindValue($locationId)
1234
                )
1235
            );
1236
        $statement = $query->prepare();
1237
        $statement->execute();
1238
1239
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1240
            return $row;
1241
        }
1242
1243
        throw new NotFound('trash', $locationId);
1244
    }
1245
1246
    /**
1247
     * List trashed items.
1248
     *
1249
     * @param int $offset
1250
     * @param int $limit
1251
     * @param array $sort
1252
     *
1253
     * @return array
1254
     */
1255
    public function listTrashed($offset, $limit, array $sort = null)
1256
    {
1257
        $query = $this->handler->createSelectQuery();
1258
        $query
1259
            ->select('*')
1260
            ->from($this->handler->quoteTable('ezcontentobject_trash'));
1261
1262
        $sort = $sort ?: array();
1263
        foreach ($sort as $condition) {
1264
            $sortDirection = $condition->direction === Query::SORT_ASC ? SelectQuery::ASC : SelectQuery::DESC;
1265
            switch (true) {
1266
                case $condition instanceof SortClause\Location\Depth:
1267
                    $query->orderBy('depth', $sortDirection);
1268
                    break;
1269
1270
                case $condition instanceof SortClause\Location\Path:
1271
                    $query->orderBy('path_string', $sortDirection);
1272
                    break;
1273
1274
                case $condition instanceof SortClause\Location\Priority:
1275
                    $query->orderBy('priority', $sortDirection);
1276
                    break;
1277
1278
                default:
1279
                    // Only handle location related sort clauses. The others
1280
                    // require data aggregation which is not sensible here.
1281
                    // Since also criteria are yet ignored, because they are
1282
                    // simply not used yet in eZ Publish, we skip that for now.
1283
                    throw new RuntimeException('Unhandled sort clause: ' . get_class($condition));
1284
            }
1285
        }
1286
1287
        if ($limit !== null) {
1288
            $query->limit($limit, $offset);
1289
        }
1290
1291
        $statement = $query->prepare();
1292
        $statement->execute();
1293
1294
        $rows = array();
1295
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1296
            $rows[] = $row;
1297
        }
1298
1299
        return $rows;
1300
    }
1301
1302
    /**
1303
     * Removes every entries in the trash.
1304
     * Will NOT remove associated content objects nor attributes.
1305
     *
1306
     * Basically truncates ezcontentobject_trash table.
1307
     */
1308
    public function cleanupTrash()
1309
    {
1310
        $query = $this->handler->createDeleteQuery();
1311
        $query->deleteFrom('ezcontentobject_trash');
1312
        $query->prepare()->execute();
1313
    }
1314
1315
    /**
1316
     * Removes trashed element identified by $id from trash.
1317
     * Will NOT remove associated content object nor attributes.
1318
     *
1319
     * @param int $id The trashed location Id
1320
     */
1321
    public function removeElementFromTrash($id)
1322
    {
1323
        $query = $this->handler->createDeleteQuery();
1324
        $query
1325
            ->deleteFrom('ezcontentobject_trash')
1326
            ->where(
1327
                $query->expr->eq(
1328
                    $this->handler->quoteColumn('node_id'),
1329
                    $query->bindValue($id, null, \PDO::PARAM_INT)
1330
                )
1331
            );
1332
        $query->prepare()->execute();
1333
    }
1334
1335
    /**
1336
     * Set section on all content objects in the subtree.
1337
     *
1338
     * @param string $pathString
1339
     * @param int $sectionId
1340
     *
1341
     * @return bool
1342
     */
1343
    public function setSectionForSubtree($pathString, $sectionId)
1344
    {
1345
        $selectContentIdsQuery = $this->connection->createQueryBuilder();
1346
        $selectContentIdsQuery
1347
            ->select('t.contentobject_id')
1348
            ->from('ezcontentobject_tree', 't')
1349
            ->where(
1350
                $selectContentIdsQuery->expr()->like(
1351
                    't.path_string',
1352
                    $selectContentIdsQuery->createPositionalParameter("{$pathString}%")
1353
                )
1354
            );
1355
1356
        $contentIds = array_map(
1357
            'intval',
1358
            $selectContentIdsQuery->execute()->fetchAll(PDO::FETCH_COLUMN)
1359
        );
1360
1361
        if (empty($contentIds)) {
1362
            return false;
1363
        }
1364
1365
        $updateSectionQuery = $this->connection->createQueryBuilder();
1366
        $updateSectionQuery
1367
            ->update('ezcontentobject')
1368
            ->set(
1369
                'section_id',
1370
                $updateSectionQuery->createPositionalParameter($sectionId, PDO::PARAM_INT)
1371
            )
1372
            ->where(
1373
                $updateSectionQuery->expr()->in(
1374
                    'id',
1375
                    $contentIds
1376
                )
1377
            );
1378
        $affectedRows = $updateSectionQuery->execute();
1379
1380
        return $affectedRows > 0;
1381
    }
1382
1383
    /**
1384
     * Returns how many locations given content object identified by $contentId has.
1385
     *
1386
     * @param int $contentId
1387
     *
1388
     * @return int
1389
     */
1390
    public function countLocationsByContentId($contentId)
1391
    {
1392
        $q = $this->handler->createSelectQuery();
1393
        $q
1394
            ->select(
1395
                $q->alias($q->expr->count('*'), 'count')
1396
            )
1397
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
1398
            ->where(
1399
                $q->expr->eq(
1400
                    $this->handler->quoteColumn('contentobject_id'),
1401
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1402
                )
1403
            );
1404
        $stmt = $q->prepare();
1405
        $stmt->execute();
1406
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1407
1408
        return (int)$res[0]['count'];
1409
    }
1410
1411
    /**
1412
     * Changes main location of content identified by given $contentId to location identified by given $locationId.
1413
     *
1414
     * Updates ezcontentobject_tree table for the given $contentId and eznode_assignment table for the given
1415
     * $contentId, $parentLocationId and $versionNo
1416
     *
1417
     * @param mixed $contentId
1418
     * @param mixed $locationId
1419
     * @param mixed $versionNo version number, needed to update eznode_assignment table
1420
     * @param mixed $parentLocationId parent location of location identified by $locationId, needed to update
1421
     *        eznode_assignment table
1422
     */
1423
    public function changeMainLocation($contentId, $locationId, $versionNo, $parentLocationId)
1424
    {
1425
        // Update ezcontentobject_tree table
1426
        $q = $this->handler->createUpdateQuery();
1427
        $q->update(
1428
            $this->handler->quoteTable('ezcontentobject_tree')
1429
        )->set(
1430
            $this->handler->quoteColumn('main_node_id'),
1431
            $q->bindValue($locationId, null, \PDO::PARAM_INT)
1432
        )->where(
1433
            $q->expr->eq(
1434
                $this->handler->quoteColumn('contentobject_id'),
1435
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
1436
            )
1437
        );
1438
        $q->prepare()->execute();
1439
1440
        // Erase is_main in eznode_assignment table
1441
        $q = $this->handler->createUpdateQuery();
1442
        $q->update(
1443
            $this->handler->quoteTable('eznode_assignment')
1444
        )->set(
1445
            $this->handler->quoteColumn('is_main'),
1446
            $q->bindValue(0, null, \PDO::PARAM_INT)
1447
        )->where(
1448
            $q->expr->lAnd(
1449
                $q->expr->eq(
1450
                    $this->handler->quoteColumn('contentobject_id'),
1451
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1452
                ),
1453
                $q->expr->eq(
1454
                    $this->handler->quoteColumn('contentobject_version'),
1455
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1456
                ),
1457
                $q->expr->neq(
1458
                    $this->handler->quoteColumn('parent_node'),
1459
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1460
                )
1461
            )
1462
        );
1463
        $q->prepare()->execute();
1464
1465
        // Set new is_main in eznode_assignment table
1466
        $q = $this->handler->createUpdateQuery();
1467
        $q->update(
1468
            $this->handler->quoteTable('eznode_assignment')
1469
        )->set(
1470
            $this->handler->quoteColumn('is_main'),
1471
            $q->bindValue(1, null, \PDO::PARAM_INT)
1472
        )->where(
1473
            $q->expr->lAnd(
1474
                $q->expr->eq(
1475
                    $this->handler->quoteColumn('contentobject_id'),
1476
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1477
                ),
1478
                $q->expr->eq(
1479
                    $this->handler->quoteColumn('contentobject_version'),
1480
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1481
                ),
1482
                $q->expr->eq(
1483
                    $this->handler->quoteColumn('parent_node'),
1484
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1485
                )
1486
            )
1487
        );
1488
        $q->prepare()->execute();
1489
    }
1490
1491
    /**
1492
     * Get the total number of all Locations, except the Root node.
1493
     *
1494
     * @see loadAllLocationsData
1495
     *
1496
     * @return int
1497
     */
1498
    public function countAllLocations()
1499
    {
1500
        $query = $this->getAllLocationsQueryBuilder(['count(node_id)']);
1501
1502
        $statement = $query->execute();
1503
1504
        return (int) $statement->fetch(PDO::FETCH_COLUMN);
1505
    }
1506
1507
    /**
1508
     * Load data of every Location, except the Root node.
1509
     *
1510
     * @param int $offset Paginator offset
1511
     * @param int $limit Paginator limit
1512
     *
1513
     * @return array
1514
     */
1515
    public function loadAllLocationsData($offset, $limit)
1516
    {
1517
        $query = $this
1518
            ->getAllLocationsQueryBuilder(
1519
                [
1520
                    'node_id',
1521
                    'priority',
1522
                    'is_hidden',
1523
                    'is_invisible',
1524
                    'remote_id',
1525
                    'contentobject_id',
1526
                    'parent_node_id',
1527
                    'path_identification_string',
1528
                    'path_string',
1529
                    'depth',
1530
                    'sort_field',
1531
                    'sort_order',
1532
                ]
1533
            )
1534
            ->setFirstResult($offset)
1535
            ->setMaxResults($limit)
1536
            ->orderBy('depth', 'ASC')
1537
            ->addOrderBy('node_id', 'ASC')
1538
        ;
1539
1540
        $statement = $query->execute();
1541
1542
        return $statement->fetchAll(PDO::FETCH_ASSOC);
1543
    }
1544
1545
    /**
1546
     * Get Query Builder for fetching data of all Locations except the Root node.
1547
     *
1548
     * @todo Align with createNodeQueryBuilder, removing the need for both(?)
1549
     *
1550
     * @param array $columns list of columns to fetch
1551
     *
1552
     * @return \Doctrine\DBAL\Query\QueryBuilder
1553
     */
1554
    private function getAllLocationsQueryBuilder(array $columns)
1555
    {
1556
        $query = $this->connection->createQueryBuilder();
1557
        $query
1558
            ->select($columns)
1559
            ->from('ezcontentobject_tree')
1560
            ->where($query->expr()->neq('node_id', 'parent_node_id'))
1561
        ;
1562
1563
        return $query;
1564
    }
1565
1566
    /**
1567
     * Create QueryBuilder for selecting node data.
1568
     *
1569
     * @param array|null $translations Filters on language mask of content if provided.
1570
     * @param bool $useAlwaysAvailable Respect always available flag on content when filtering on $translations.
1571
     *
1572
     * @return \Doctrine\DBAL\Query\QueryBuilder
1573
     */
1574
    private function createNodeQueryBuilder(array $translations = null, bool $useAlwaysAvailable = true): QueryBuilder
1575
    {
1576
        $queryBuilder = $this->connection->createQueryBuilder();
1577
        $queryBuilder
1578
            ->select('t.*')
1579
            ->from('ezcontentobject_tree', 't');
1580
1581
        if (!empty($translations)) {
1582
            $dbPlatform = $this->connection->getDatabasePlatform();
1583
            $expr = $queryBuilder->expr();
1584
            $mask = $this->generateLanguageMaskFromLanguageCodes($translations, $useAlwaysAvailable);
1585
1586
            $queryBuilder->innerJoin(
1587
                't',
1588
                'ezcontentobject',
1589
                'c',
1590
                $expr->andX(
1591
                    $expr->eq('t.contentobject_id', 'c.id'),
1592
                    $expr->gt(
1593
                        $dbPlatform->getBitAndComparisonExpression('c.language_mask', $mask),
1594
                        0
1595
                    )
1596
                )
1597
            );
1598
        }
1599
1600
        return $queryBuilder;
1601
    }
1602
1603
    /**
1604
     * Generates a language mask for $translations argument.
1605
     *
1606
     * @todo Move logic to languageMaskGenerator in master.
1607
     */
1608 View Code Duplication
    private function generateLanguageMaskFromLanguageCodes(array $translations, bool $useAlwaysAvailable = true): int
1609
    {
1610
        $languages = [];
1611
        foreach ($translations as $translation) {
1612
            if (isset($languages[$translation])) {
1613
                continue;
1614
            }
1615
1616
            $languages[$translation] = true;
1617
        }
1618
1619
        if ($useAlwaysAvailable) {
1620
            $languages['always-available'] = true;
1621
        }
1622
1623
        return $this->languageMaskGenerator->generateLanguageMask($languages);
1624
    }
1625
}
1626