Completed
Push — location_multi_load ( e5e305 )
by André
32:49 queued 12:49
created

DoctrineDatabase   F

Complexity

Total Complexity 86

Size/Duplication

Total Lines 1595
Duplicated Lines 5.02 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
dl 80
loc 1595
c 0
b 0
f 0
rs 0.8
wmc 86
lcom 1
cbo 16

41 Methods

Rating   Name   Duplication   Size   Complexity  
A getAllLocationsQueryBuilder() 0 11 1
A loadLocationDataByContent() 0 22 2
A loadParentLocationsDataForDraftContent() 0 45 1
A getSubtreeContent() 0 19 4
A applySubtreeLimitation() 0 9 1
A getChildren() 0 16 1
C moveSubtreeNodes() 0 94 8
A isHiddenByParent() 0 12 4
A updateSubtreeModificationTime() 0 20 2
A hideSubtree() 0 36 1
B unHideSubtree() 0 105 3
B swap() 0 60 2
B create() 0 81 2
A createNodeAssignment() 0 53 2
A deleteNodeAssignment() 0 21 2
A updateNodeAssignment() 0 27 1
B createLocationsFromNodeAssignments() 0 62 5
A updateLocationsContentVersionNo() 16 16 1
A getMainNodeId() 0 28 2
A update() 0 38 1
A updatePathIdentificationString() 0 23 2
A removeLocation() 0 13 1
A getFallbackMainNodeData() 0 26 1
A trashLocation() 0 18 2
A untrashLocation() 0 26 2
A setContentStatus() 0 17 1
A loadTrashByLocation() 21 21 2
B listTrashed() 0 46 9
A cleanupTrash() 0 6 1
A removeElementFromTrash() 0 13 1
A setSectionForSubtree() 0 39 2
A countLocationsByContentId() 0 20 1
B changeMainLocation() 0 67 1
A countAllLocations() 0 8 1
A loadAllLocationsData() 0 29 1
A __construct() 0 6 1
A getBasicNodeData() 13 13 2
A getNodeDataList() 0 12 1
A getBasicNodeDataByRemoteId() 13 13 2
A createNodeQueryBuilder() 0 30 2
A generateLanguageMaskFromLanguageCodes() 17 17 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DoctrineDatabase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DoctrineDatabase, and based on these observations, apply Extract Interface, too.

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)
0 ignored issues
show
Deprecated Code introduced by
The property eZ\Publish\SPI\Persisten...athIdentificationString has been deprecated with message: Since 5.4, planned to be removed in 6.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
Deprecated Code introduced by
The property eZ\Publish\SPI\Persisten...athIdentificationString has been deprecated with message: Since 5.4, planned to be removed in 6.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
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
        foreach ($locationRow as $key => $value) {
1145
            $query->set($key, $query->bindValue($value));
1146
        }
1147
1148
        $query->prepare()->execute();
1149
1150
        $this->removeLocation($locationRow['node_id']);
1151
        $this->setContentStatus($locationRow['contentobject_id'], ContentInfo::STATUS_TRASHED);
1152
    }
1153
1154
    /**
1155
     * Returns a trashed location to normal state.
1156
     *
1157
     * Recreates the originally trashed location in the new position. If no new
1158
     * position has been specified, it will be tried to re-create the location
1159
     * at the old position. If this is not possible ( because the old location
1160
     * does not exist any more) and exception is thrown.
1161
     *
1162
     * @param mixed $locationId
1163
     * @param mixed|null $newParentId
1164
     *
1165
     * @return \eZ\Publish\SPI\Persistence\Content\Location
1166
     */
1167
    public function untrashLocation($locationId, $newParentId = null)
1168
    {
1169
        $row = $this->loadTrashByLocation($locationId);
1170
1171
        $newLocation = $this->create(
1172
            new CreateStruct(
1173
                array(
1174
                    'priority' => $row['priority'],
1175
                    'hidden' => $row['is_hidden'],
1176
                    'invisible' => $row['is_invisible'],
1177
                    'remoteId' => $row['remote_id'],
1178
                    'contentId' => $row['contentobject_id'],
1179
                    'contentVersion' => $row['contentobject_version'],
1180
                    'mainLocationId' => true, // Restored location is always main location
1181
                    'sortField' => $row['sort_field'],
1182
                    'sortOrder' => $row['sort_order'],
1183
                )
1184
            ),
1185
            $this->getBasicNodeData($newParentId ?: $row['parent_node_id'])
1186
        );
1187
1188
        $this->removeElementFromTrash($locationId);
1189
        $this->setContentStatus($row['contentobject_id'], ContentInfo::STATUS_PUBLISHED);
1190
1191
        return $newLocation;
1192
    }
1193
1194
    /**
1195
     * @param mixed $contentId
1196
     * @param int $status
1197
     */
1198
    protected function setContentStatus($contentId, $status)
1199
    {
1200
        /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1201
        $query = $this->handler->createUpdateQuery();
1202
        $query->update(
1203
            'ezcontentobject'
1204
        )->set(
1205
            $this->handler->quoteColumn('status'),
1206
            $query->bindValue($status, null, \PDO::PARAM_INT)
1207
        )->where(
1208
            $query->expr->eq(
1209
                $this->handler->quoteColumn('id'),
1210
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1211
            )
1212
        );
1213
        $query->prepare()->execute();
1214
    }
1215
1216
    /**
1217
     * Loads trash data specified by location ID.
1218
     *
1219
     * @param mixed $locationId
1220
     *
1221
     * @return array
1222
     */
1223 View Code Duplication
    public function loadTrashByLocation($locationId)
1224
    {
1225
        $query = $this->handler->createSelectQuery();
1226
        $query
1227
            ->select('*')
1228
            ->from($this->handler->quoteTable('ezcontentobject_trash'))
1229
            ->where(
1230
                $query->expr->eq(
1231
                    $this->handler->quoteColumn('node_id'),
1232
                    $query->bindValue($locationId)
1233
                )
1234
            );
1235
        $statement = $query->prepare();
1236
        $statement->execute();
1237
1238
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1239
            return $row;
1240
        }
1241
1242
        throw new NotFound('trash', $locationId);
1243
    }
1244
1245
    /**
1246
     * List trashed items.
1247
     *
1248
     * @param int $offset
1249
     * @param int $limit
1250
     * @param array $sort
1251
     *
1252
     * @return array
1253
     */
1254
    public function listTrashed($offset, $limit, array $sort = null)
1255
    {
1256
        $query = $this->handler->createSelectQuery();
1257
        $query
1258
            ->select('*')
1259
            ->from($this->handler->quoteTable('ezcontentobject_trash'));
1260
1261
        $sort = $sort ?: array();
1262
        foreach ($sort as $condition) {
1263
            $sortDirection = $condition->direction === Query::SORT_ASC ? SelectQuery::ASC : SelectQuery::DESC;
1264
            switch (true) {
1265
                case $condition instanceof SortClause\Location\Depth:
1266
                    $query->orderBy('depth', $sortDirection);
1267
                    break;
1268
1269
                case $condition instanceof SortClause\Location\Path:
1270
                    $query->orderBy('path_string', $sortDirection);
1271
                    break;
1272
1273
                case $condition instanceof SortClause\Location\Priority:
1274
                    $query->orderBy('priority', $sortDirection);
1275
                    break;
1276
1277
                default:
1278
                    // Only handle location related sort clauses. The others
1279
                    // require data aggregation which is not sensible here.
1280
                    // Since also criteria are yet ignored, because they are
1281
                    // simply not used yet in eZ Publish, we skip that for now.
1282
                    throw new RuntimeException('Unhandled sort clause: ' . get_class($condition));
1283
            }
1284
        }
1285
1286
        if ($limit !== null) {
1287
            $query->limit($limit, $offset);
1288
        }
1289
1290
        $statement = $query->prepare();
1291
        $statement->execute();
1292
1293
        $rows = array();
1294
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
1295
            $rows[] = $row;
1296
        }
1297
1298
        return $rows;
1299
    }
1300
1301
    /**
1302
     * Removes every entries in the trash.
1303
     * Will NOT remove associated content objects nor attributes.
1304
     *
1305
     * Basically truncates ezcontentobject_trash table.
1306
     */
1307
    public function cleanupTrash()
1308
    {
1309
        $query = $this->handler->createDeleteQuery();
1310
        $query->deleteFrom('ezcontentobject_trash');
1311
        $query->prepare()->execute();
1312
    }
1313
1314
    /**
1315
     * Removes trashed element identified by $id from trash.
1316
     * Will NOT remove associated content object nor attributes.
1317
     *
1318
     * @param int $id The trashed location Id
1319
     */
1320
    public function removeElementFromTrash($id)
1321
    {
1322
        $query = $this->handler->createDeleteQuery();
1323
        $query
1324
            ->deleteFrom('ezcontentobject_trash')
1325
            ->where(
1326
                $query->expr->eq(
1327
                    $this->handler->quoteColumn('node_id'),
1328
                    $query->bindValue($id, null, \PDO::PARAM_INT)
1329
                )
1330
            );
1331
        $query->prepare()->execute();
1332
    }
1333
1334
    /**
1335
     * Set section on all content objects in the subtree.
1336
     *
1337
     * @param string $pathString
1338
     * @param int $sectionId
1339
     *
1340
     * @return bool
1341
     */
1342
    public function setSectionForSubtree($pathString, $sectionId)
1343
    {
1344
        $selectContentIdsQuery = $this->connection->createQueryBuilder();
1345
        $selectContentIdsQuery
1346
            ->select('t.contentobject_id')
1347
            ->from('ezcontentobject_tree', 't')
1348
            ->where(
1349
                $selectContentIdsQuery->expr()->like(
1350
                    't.path_string',
1351
                    $selectContentIdsQuery->createPositionalParameter("{$pathString}%")
1352
                )
1353
            );
1354
1355
        $contentIds = array_map(
1356
            'intval',
1357
            $selectContentIdsQuery->execute()->fetchAll(PDO::FETCH_COLUMN)
1358
        );
1359
1360
        if (empty($contentIds)) {
1361
            return false;
1362
        }
1363
1364
        $updateSectionQuery = $this->connection->createQueryBuilder();
1365
        $updateSectionQuery
1366
            ->update('ezcontentobject')
1367
            ->set(
1368
                'section_id',
1369
                $updateSectionQuery->createPositionalParameter($sectionId, PDO::PARAM_INT)
1370
            )
1371
            ->where(
1372
                $updateSectionQuery->expr()->in(
1373
                    'id',
1374
                    $contentIds
1375
                )
1376
            );
1377
        $affectedRows = $updateSectionQuery->execute();
1378
1379
        return $affectedRows > 0;
1380
    }
1381
1382
    /**
1383
     * Returns how many locations given content object identified by $contentId has.
1384
     *
1385
     * @param int $contentId
1386
     *
1387
     * @return int
1388
     */
1389
    public function countLocationsByContentId($contentId)
1390
    {
1391
        $q = $this->handler->createSelectQuery();
1392
        $q
1393
            ->select(
1394
                $q->alias($q->expr->count('*'), 'count')
1395
            )
1396
            ->from($this->handler->quoteTable('ezcontentobject_tree'))
1397
            ->where(
1398
                $q->expr->eq(
1399
                    $this->handler->quoteColumn('contentobject_id'),
1400
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1401
                )
1402
            );
1403
        $stmt = $q->prepare();
1404
        $stmt->execute();
1405
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1406
1407
        return (int)$res[0]['count'];
1408
    }
1409
1410
    /**
1411
     * Changes main location of content identified by given $contentId to location identified by given $locationId.
1412
     *
1413
     * Updates ezcontentobject_tree table for the given $contentId and eznode_assignment table for the given
1414
     * $contentId, $parentLocationId and $versionNo
1415
     *
1416
     * @param mixed $contentId
1417
     * @param mixed $locationId
1418
     * @param mixed $versionNo version number, needed to update eznode_assignment table
1419
     * @param mixed $parentLocationId parent location of location identified by $locationId, needed to update
1420
     *        eznode_assignment table
1421
     */
1422
    public function changeMainLocation($contentId, $locationId, $versionNo, $parentLocationId)
1423
    {
1424
        // Update ezcontentobject_tree table
1425
        $q = $this->handler->createUpdateQuery();
1426
        $q->update(
1427
            $this->handler->quoteTable('ezcontentobject_tree')
1428
        )->set(
1429
            $this->handler->quoteColumn('main_node_id'),
1430
            $q->bindValue($locationId, null, \PDO::PARAM_INT)
1431
        )->where(
1432
            $q->expr->eq(
1433
                $this->handler->quoteColumn('contentobject_id'),
1434
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
1435
            )
1436
        );
1437
        $q->prepare()->execute();
1438
1439
        // Erase is_main in eznode_assignment table
1440
        $q = $this->handler->createUpdateQuery();
1441
        $q->update(
1442
            $this->handler->quoteTable('eznode_assignment')
1443
        )->set(
1444
            $this->handler->quoteColumn('is_main'),
1445
            $q->bindValue(0, null, \PDO::PARAM_INT)
1446
        )->where(
1447
            $q->expr->lAnd(
1448
                $q->expr->eq(
1449
                    $this->handler->quoteColumn('contentobject_id'),
1450
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1451
                ),
1452
                $q->expr->eq(
1453
                    $this->handler->quoteColumn('contentobject_version'),
1454
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1455
                ),
1456
                $q->expr->neq(
1457
                    $this->handler->quoteColumn('parent_node'),
1458
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1459
                )
1460
            )
1461
        );
1462
        $q->prepare()->execute();
1463
1464
        // Set new is_main in eznode_assignment table
1465
        $q = $this->handler->createUpdateQuery();
1466
        $q->update(
1467
            $this->handler->quoteTable('eznode_assignment')
1468
        )->set(
1469
            $this->handler->quoteColumn('is_main'),
1470
            $q->bindValue(1, null, \PDO::PARAM_INT)
1471
        )->where(
1472
            $q->expr->lAnd(
1473
                $q->expr->eq(
1474
                    $this->handler->quoteColumn('contentobject_id'),
1475
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
1476
                ),
1477
                $q->expr->eq(
1478
                    $this->handler->quoteColumn('contentobject_version'),
1479
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
1480
                ),
1481
                $q->expr->eq(
1482
                    $this->handler->quoteColumn('parent_node'),
1483
                    $q->bindValue($parentLocationId, null, \PDO::PARAM_INT)
1484
                )
1485
            )
1486
        );
1487
        $q->prepare()->execute();
1488
    }
1489
1490
    /**
1491
     * Get the total number of all Locations, except the Root node.
1492
     *
1493
     * @see loadAllLocationsData
1494
     *
1495
     * @return int
1496
     */
1497
    public function countAllLocations()
1498
    {
1499
        $query = $this->getAllLocationsQueryBuilder(['count(node_id)']);
1500
1501
        $statement = $query->execute();
1502
1503
        return (int) $statement->fetch(PDO::FETCH_COLUMN);
1504
    }
1505
1506
    /**
1507
     * Load data of every Location, except the Root node.
1508
     *
1509
     * @param int $offset Paginator offset
1510
     * @param int $limit Paginator limit
1511
     *
1512
     * @return array
1513
     */
1514
    public function loadAllLocationsData($offset, $limit)
1515
    {
1516
        $query = $this
1517
            ->getAllLocationsQueryBuilder(
1518
                [
1519
                    'node_id',
1520
                    'priority',
1521
                    'is_hidden',
1522
                    'is_invisible',
1523
                    'remote_id',
1524
                    'contentobject_id',
1525
                    'parent_node_id',
1526
                    'path_identification_string',
1527
                    'path_string',
1528
                    'depth',
1529
                    'sort_field',
1530
                    'sort_order',
1531
                ]
1532
            )
1533
            ->setFirstResult($offset)
1534
            ->setMaxResults($limit)
1535
            ->orderBy('depth', 'ASC')
1536
            ->addOrderBy('node_id', 'ASC')
1537
        ;
1538
1539
        $statement = $query->execute();
1540
1541
        return $statement->fetchAll(PDO::FETCH_ASSOC);
1542
    }
1543
1544
    /**
1545
     * Get Query Builder for fetching data of all Locations except the Root node.
1546
     *
1547
     * @todo Align with createNodeQueryBuilder, removing the need for both(?)
1548
     *
1549
     * @param array $columns list of columns to fetch
1550
     *
1551
     * @return \Doctrine\DBAL\Query\QueryBuilder
1552
     */
1553
    private function getAllLocationsQueryBuilder(array $columns)
1554
    {
1555
        $query = $this->connection->createQueryBuilder();
1556
        $query
1557
            ->select($columns)
1558
            ->from('ezcontentobject_tree')
1559
            ->where($query->expr()->neq('node_id', 'parent_node_id'))
1560
        ;
1561
1562
        return $query;
1563
    }
1564
1565
    /**
1566
     * Create QueryBuilder for selecting node data.
1567
     *
1568
     * @param array|null $translations Filters on language mask of content if provided.
1569
     * @param bool $useAlwaysAvailable Respect always available flag on content when filtering on $translations.
1570
     *
1571
     * @return \Doctrine\DBAL\Query\QueryBuilder
1572
     */
1573
    private function createNodeQueryBuilder(array $translations = null, bool $useAlwaysAvailable = true): QueryBuilder
1574
    {
1575
        $queryBuilder = $this->connection->createQueryBuilder();
1576
        $queryBuilder
1577
            ->select('t.*')
1578
            ->from('ezcontentobject_tree', 't');
1579
1580
        if (!empty($translations)) {
1581
            $dbPlatform = $this->connection->getDatabasePlatform();
1582
            $expr = $queryBuilder->expr();
1583
            $mask = $this->generateLanguageMaskFromLanguageCodes($translations, $useAlwaysAvailable);
1584
1585
            $queryBuilder->innerJoin(
1586
                't',
1587
                'ezcontentobject',
1588
                'c',
1589
                $expr->andX(
1590
                    $expr->eq('t.contentobject_id', 'c.id'),
1591
                    // Won't work on Oracle, consider contribute bitwise features to query builder, and
1592
                    // contribute support for it handling DBM differences. Or detect Oracle here and do bitand()
1593
                    $expr->gt(
1594
                        $dbPlatform->getBitAndComparisonExpression('c.language_mask', $mask),
1595
                        0
1596
                    )
1597
                )
1598
            );
1599
        }
1600
1601
        return $queryBuilder;
1602
    }
1603
1604
    /**
1605
     * Generates a language mask for $translations argument.
1606
     *
1607
     * @todo Move logic to languageMaskGenerator in master.
1608
     */
1609 View Code Duplication
    private function generateLanguageMaskFromLanguageCodes(array $translations, bool $useAlwaysAvailable = true): int
1610
    {
1611
        $languages = [];
1612
        foreach ($translations as $translation) {
1613
            if (isset($languages[$translation])) {
1614
                continue;
1615
            }
1616
1617
            $languages[$translation] = true;
1618
        }
1619
1620
        if ($useAlwaysAvailable) {
1621
            $languages['always-available'] = true;
1622
        }
1623
1624
        return $this->languageMaskGenerator->generateLanguageMask($languages);
1625
    }
1626
}
1627