Completed
Push — master ( a6387c...f7252d )
by André
23:52 queued 09:38
created

RegenerateUrlAliasesCommand::setDefaultTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of the eZ Publish Kernel package.
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\Bundle\EzPublishMigrationBundle\Command\LegacyStorage;
10
11
use eZ\Publish\API\Repository\Exceptions\ForbiddenException;
12
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway as UrlAliasGateway;
13
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler as UrlAliasHandler;
14
use eZ\Publish\Core\Persistence\Legacy\Handler as LegacyStorageEngine;
15
use eZ\Publish\SPI\Persistence\Content\UrlAlias;
16
use Doctrine\DBAL\Query\QueryBuilder;
17
use Doctrine\DBAL\Schema\Schema;
18
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
19
use Symfony\Component\Console\Input\InputArgument;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Helper\ProgressBar;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use RuntimeException;
24
use Exception;
25
use PDO;
26
27
class RegenerateUrlAliasesCommand extends ContainerAwareCommand
28
{
29
    const MIGRATION_TABLE = '__migration_ezurlalias_ml';
30
    const CUSTOM_ALIAS_BACKUP_TABLE = '__migration_backup_custom_alias';
31
    const GLOBAL_ALIAS_BACKUP_TABLE = '__migration_backup_global_alias';
32
33
    /**
34
     * @var \eZ\Publish\API\Repository\ContentService
35
     */
36
    protected $contentService;
37
38
    /**
39
     * @var \eZ\Publish\Core\Repository\Helper\NameSchemaService
40
     */
41
    protected $nameSchemaResolver;
42
43
    /**
44
     * @var \eZ\Publish\SPI\Persistence\Content\UrlAlias\Handler
45
     */
46
    protected $urlAliasHandler;
47
48
    /**
49
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
50
     */
51
    protected $urlAliasGateway;
52
53
    /**
54
     * @var \Doctrine\DBAL\Connection
55
     */
56
    protected $connection;
57
58
    /**
59
     * @var int
60
     */
61
    protected $bulkCount;
62
63
    /**
64
     * @var \Symfony\Component\Console\Output\OutputInterface
65
     */
66
    protected $output;
67
68
    protected $actionSet = [
69
        'full' => true,
70
        'autogenerate' => true,
71
        'backup-custom' => true,
72
        'restore-custom' => true,
73
        'backup-global' => true,
74
        'restore-global' => true,
75
    ];
76
77
    protected function configure()
78
    {
79
        $this
80
            ->setName('ezplatform:regenerate:legacy_storage_url_aliases')
81
            ->setDescription(
82
                'Regenerates Location URL aliases (autogenerated) and migrates custom Location ' .
83
                'and global URL aliases with Legacy Storage Engine'
84
            )
85
            ->addArgument(
86
                'action',
87
                InputArgument::REQUIRED,
88
                'Action to perform, one of: full, autogenerate, backup-custom, restore-custom, backup-global, restore-global'
89
            )
90
            ->addArgument(
91
                'bulk-count',
92
                InputArgument::OPTIONAL,
93
                'Number of items (Locations, URL aliases) processed at once',
94
                50
95
            )
96
            ->setHelp(
97
                <<<EOT
98
The command <info>%command.name%</info> regenerates URL aliases for Locations
99
and migrates existing custom Location and global URL aliases to a separate database table. Separate
100
table must be named '__migration_ezurlalias_ml' and should be created manually to be identical (but
101
empty) as the existing table 'ezurlalias_ml' before the command is executed.
102
103
After the script finishes, to complete migration the table should be renamed to 'ezurlalias_ml'
104
manually.
105
106
Using available options for 'action' argument, you can backup custom Location and global URL aliases
107
separately and inspect them before restoring them to the migration table. They will be stored in
108
backup tables named '__migration_backup_custom_alias' and '__migration_backup_global_alias' (created
109
automatically).
110
111
It is also possible to skip custom Location and global URL aliases altogether and regenerate only
112
automatically created URL aliases for Locations (use 'autogenerate' action to achieve this).
113
114
<error>During the script execution the database should not be modified.</error>
115
116
Since this script can potentially run for a very long time, to avoid memory exhaustion run it in
117
production environment using <info>--env=prod</info> switch.
118
119
EOT
120
            );
121
    }
122
123
    protected function execute(InputInterface $input, OutputInterface $output)
124
    {
125
        $this->checkStorage();
126
127
        $action = $input->getArgument('action');
128
        $this->bulkCount = $input->getArgument('bulk-count');
129
130
        if (!isset($this->actionSet[$action])) {
131
            throw new RuntimeException(
132
                "Action '{$action}' is not supported, use one of: " .
133
                implode(', ', array_keys($this->actionSet))
134
            );
135
        }
136
137
        if ($action === 'full' || $action === 'backup-custom') {
138
            $this->backupCustomLocationAliases();
139
        }
140
141
        if ($action === 'full' || $action === 'backup-global') {
142
            $this->backupGlobalAliases();
143
        }
144
145
        if ($action === 'full' || $action === 'autogenerate') {
146
            $this->generateLocationAliases();
147
        }
148
149
        if ($action === 'full' || $action === 'restore-custom') {
150
            $this->restoreCustomLocationAliases();
151
        }
152
153
        if ($action === 'full' || $action === 'restore-global') {
154
            $this->restoreGlobalAliases();
155
        }
156
    }
157
158
    protected function initialize(InputInterface $input, OutputInterface $output)
159
    {
160
        /** @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler $databaseHandler */
161
        $databaseHandler = $this->getContainer()->get('ezpublish.connection');
162
        /** @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway */
163
        $gateway = $this->getContainer()->get('ezpublish.persistence.legacy.url_alias.gateway');
164
        /** @var \eZ\Publish\SPI\Persistence\Handler $persistenceHandler */
165
        $persistenceHandler = $this->getContainer()->get('ezpublish.api.persistence_handler');
166
        /** @var \eZ\Publish\Core\Repository\Repository $innerRepository */
167
        $innerRepository = $this->getContainer()->get('ezpublish.api.inner_repository');
168
        /** @var \eZ\Publish\API\Repository\Repository $repository */
169
        $repository = $this->getContainer()->get('ezpublish.api.repository');
170
171
        $administratorUser = $repository->getUserService()->loadUser(14);
172
        $repository->getPermissionResolver()->setCurrentUserReference($administratorUser);
173
174
        $this->contentService = $repository->getContentService();
175
        $this->nameSchemaResolver = $innerRepository->getNameSchemaService();
176
        $this->urlAliasHandler = $persistenceHandler->urlAliasHandler();
177
        $this->urlAliasGateway = $gateway;
178
        $this->connection = $databaseHandler->getConnection();
179
        $this->output = $output;
180
    }
181
182
    /**
183
     * Checks that configured storage engine is Legacy Storage Engine.
184
     */
185
    protected function checkStorage()
186
    {
187
        $storageEngine = $this->getContainer()->get('ezpublish.api.storage_engine');
188
189
        if (!$storageEngine instanceof LegacyStorageEngine) {
190
            throw new RuntimeException(
191
                'Expected to find Legacy Storage Engine but found something else.'
192
            );
193
        }
194
    }
195
196
    /**
197
     * Sets storage gateway to the default table.
198
     *
199
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway::TABLE
200
     */
201
    protected function setDefaultTable()
202
    {
203
        $this->urlAliasGateway->setTable(UrlAliasGateway::TABLE);
204
    }
205
206
    /**
207
     * Sets storage gateway to the migration table.
208
     *
209
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::MIGRATION_TABLE
210
     */
211
    protected function setMigrationTable()
212
    {
213
        $this->urlAliasGateway->setTable(static::MIGRATION_TABLE);
214
    }
215
216
    /**
217
     * Backups custom Location URL aliases the custom URL alias backup table.
218
     */
219 View Code Duplication
    protected function backupCustomLocationAliases()
220
    {
221
        $table = static::CUSTOM_ALIAS_BACKUP_TABLE;
222
223
        if (!$this->tableExists($table)) {
224
            $this->createCustomLocationUrlAliasBackupTable();
225
        }
226
227
        if (!$this->isTableEmpty($table)) {
228
            throw new RuntimeException(
229
                "Table '{$table}' contains data. " .
230
                "Ensure it's empty or non-existent (it will be automatically created)."
231
            );
232
        }
233
234
        $this->doBackupCustomLocationAliases();
235
    }
236
237
    /**
238
     * Internal method for backing up custom Location URL aliases.
239
     *
240
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::backupCustomLocationAliases()
241
     */
242
    protected function doBackupCustomLocationAliases()
243
    {
244
        $totalCount = $this->getTotalLocationCount();
245
        $passCount = ceil($totalCount / $this->bulkCount);
246
        $customAliasCount = 0;
247
        $customAliasPathCount = 0;
248
249
        if ($totalCount === 0) {
250
            $this->output->writeln('Could not find any Locations, nothing to backup.');
251
            $this->output->writeln('');
252
253
            return;
254
        }
255
256
        $queryBuilder = $this->connection->createQueryBuilder();
257
        $queryBuilder
258
            ->select('node_id', 'parent_node_id', 'contentobject_id')
259
            ->from('ezcontentobject_tree')
260
            ->where($queryBuilder->expr()->neq('node_id', 1))
261
            ->orderBy('depth', 'ASC')
262
            ->orderBy('node_id', 'ASC');
263
264
        $this->output->writeln("Backing up custom URL alias(es) for {$totalCount} Location(s).");
265
266
        $progressBar = $this->getProgressBar($totalCount);
267
        $progressBar->start();
268
269
        for ($pass = 0; $pass <= $passCount; ++$pass) {
270
            $rows = $this->loadLocationData($queryBuilder, $pass);
271
272
            foreach ($rows as $row) {
273
                $customAliases = $this->urlAliasHandler->listURLAliasesForLocation(
274
                    $row['node_id'],
275
                    true
276
                );
277
278
                $customAliasCount += count($customAliases);
279
                $customAliasPathCount += $this->storeCustomAliases($customAliases);
280
            }
281
282
            $progressBar->advance(count($rows));
283
        }
284
285
        $progressBar->finish();
286
287
        $this->output->writeln('');
288
        $this->output->writeln(
289
            "Done. Backed up {$customAliasCount} custom URL alias(es) " .
290
            "with {$customAliasPathCount} path(s)."
291
        );
292
        $this->output->writeln('');
293
    }
294
295
    /**
296
     * Restores custom Location URL aliases from the backup table.
297
     */
298 View Code Duplication
    protected function restoreCustomLocationAliases()
299
    {
300
        $mainTable = static::MIGRATION_TABLE;
301
        $backupTable = static::CUSTOM_ALIAS_BACKUP_TABLE;
302
303
        if (!$this->tableExists($mainTable)) {
304
            throw new RuntimeException(
305
                "Could not find main URL alias migration table '{$mainTable}'. " .
306
                'Ensure that table exists (you will have to create it manually).'
307
            );
308
        }
309
310
        if (!$this->tableExists($backupTable)) {
311
            throw new RuntimeException(
312
                "Could not find custom Location URL alias backup table '{$backupTable}'. " .
313
                "Ensure that table is created by 'backup-custom' action."
314
            );
315
        }
316
317
        $this->doRestoreCustomLocationAliases();
318
    }
319
320
    /**
321
     * Restores custom Location URL aliases from the backup table.
322
     *
323
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::restoreCustomLocationAliases()
324
     */
325 View Code Duplication
    protected function doRestoreCustomLocationAliases()
326
    {
327
        $totalCount = $this->getTotalBackupCount(static::CUSTOM_ALIAS_BACKUP_TABLE);
328
        $passCount = ceil($totalCount / $this->bulkCount);
329
        $createdAliasCount = 0;
330
        $conflictCount = 0;
331
332
        if ($totalCount === 0) {
333
            $this->output->writeln(
334
                'Could not find any backed up custom Location URL aliases, nothing to restore.'
335
            );
336
            $this->output->writeln('');
337
338
            return;
339
        }
340
341
        $queryBuilder = $this->connection->createQueryBuilder();
342
        $queryBuilder
343
            ->select('*')
344
            ->from(static::CUSTOM_ALIAS_BACKUP_TABLE)
345
            ->orderBy('id', 'ASC');
346
347
        $this->output->writeln("Restoring {$totalCount} custom URL alias(es).");
348
349
        $progressBar = $this->getProgressBar($totalCount);
350
        $progressBar->start();
351
352
        for ($pass = 0; $pass <= $passCount; ++$pass) {
353
            $rows = $this->loadPassData($queryBuilder, $pass);
354
355
            foreach ($rows as $row) {
356
                try {
357
                    $this->setMigrationTable();
358
                    $this->urlAliasHandler->createCustomUrlAlias(
359
                        $row['location_id'],
360
                        $row['path'],
361
                        (bool)$row['forwarding'],
362
                        $row['language_code'],
363
                        (bool)$row['always_available']
364
                    );
365
                    $createdAliasCount += 1;
366
                    $this->setDefaultTable();
367
                } catch (ForbiddenException $e) {
368
                    $conflictCount += 1;
369
                } catch (Exception $e) {
370
                    $this->setDefaultTable();
371
                    throw $e;
372
                }
373
            }
374
375
            $progressBar->advance(count($rows));
376
        }
377
378
        $progressBar->finish();
379
380
        $this->output->writeln('');
381
        $this->output->writeln(
382
            "Done. Restored {$createdAliasCount} custom URL alias(es) " .
383
            "with {$conflictCount} conflict(s)."
384
        );
385
        $this->output->writeln('');
386
    }
387
388
    /**
389
     * Loads Location data for the given $pass.
390
     *
391
     * @param \Doctrine\DBAL\Query\QueryBuilder $queryBuilder
392
     * @param int $pass
393
     *
394
     * @return array
395
     */
396 View Code Duplication
    protected function loadPassData(QueryBuilder $queryBuilder, $pass)
397
    {
398
        $queryBuilder->setFirstResult($pass * $this->bulkCount);
399
        $queryBuilder->setMaxResults($this->bulkCount);
400
401
        $statement = $queryBuilder->execute();
402
403
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
404
405
        return $rows;
406
    }
407
408
    /**
409
     * Stores given custom $aliases to the custom alias backup table.
410
     *
411
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $aliases
412
     *
413
     * @return int
414
     */
415
    protected function storeCustomAliases(array $aliases)
416
    {
417
        $pathCount = 0;
418
419
        foreach ($aliases as $alias) {
420
            $paths = $this->combinePaths($alias->pathData);
421
            $pathCount += count($paths);
422
423 View Code Duplication
            foreach ($paths as $path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
424
                $this->storeCustomAliasPath(
425
                    $alias->destination,
426
                    $path,
427
                    reset($alias->languageCodes),
428
                    $alias->alwaysAvailable,
429
                    $alias->forward
430
                );
431
            }
432
        }
433
434
        return $pathCount;
435
    }
436
437
    /**
438
     * Stores custom URL alias data for $path to the backup table.
439
     *
440
     * @param int $locationId
441
     * @param string $path
442
     * @param string $languageCode
443
     * @param bool $alwaysAvailable
444
     * @param bool $forwarding
445
     */
446 View Code Duplication
    protected function storeCustomAliasPath(
447
        $locationId,
448
        $path,
449
        $languageCode,
450
        $alwaysAvailable,
451
        $forwarding
452
    ) {
453
        $queryBuilder = $this->connection->createQueryBuilder();
454
455
        $queryBuilder->insert(static::CUSTOM_ALIAS_BACKUP_TABLE);
456
        $queryBuilder->values(
457
            [
458
                'id' => '?',
459
                'location_id' => '?',
460
                'path' => '?',
461
                'language_code' => '?',
462
                'always_available' => '?',
463
                'forwarding' => '?',
464
            ]
465
        );
466
        $queryBuilder->setParameter(0, 0);
467
        $queryBuilder->setParameter(1, $locationId);
468
        $queryBuilder->setParameter(2, $path);
469
        $queryBuilder->setParameter(3, $languageCode);
470
        $queryBuilder->setParameter(4, (int)$alwaysAvailable);
471
        $queryBuilder->setParameter(5, (int)$forwarding);
472
473
        $queryBuilder->execute();
474
    }
475
476
    /**
477
     * Combines path data to an array of URL alias paths.
478
     *
479
     * Explanation:
480
     *
481
     * Custom Location and global URL aliases can generate NOP entries, which can be taken over by
482
     * the autogenerated aliases. When multiple languages exists for the Location that took over,
483
     * multiple entries with the same link will exist on the same level. In that case it will not be
484
     * possible to reliably reconstruct what was the path for the original custom alias. For that
485
     * reason we combine path data to get all possible path combinations.
486
     *
487
     * Note: it could happen that original NOP entry was historized after being taken over by the
488
     * autogenerated alias. So to be complete this would have to take into account history entries
489
     * as well, but at the moment we lack API to do that.
490
     *
491
     * Proper solution of this problem would be introducing separate database table to store
492
     * custom/global URL alias data.
493
     *
494
     * @see https://jira.ez.no/browse/EZP-20777
495
     *
496
     * @param array $pathData
497
     *
498
     * @return string[]
499
     */
500
    protected function combinePaths(array $pathData)
501
    {
502
        $paths = [];
503
        $levelData = array_shift($pathData);
504
        $levelElements = $this->extractPathElements($levelData);
505
506
        if (!empty($pathData)) {
507
            $nextElements = $this->combinePaths($pathData);
508
509
            foreach ($levelElements as $element1) {
510
                foreach ($nextElements as $element2) {
511
                    $paths[] = $element1 . '/' . $element2;
512
                }
513
            }
514
515
            return $paths;
516
        }
517
518
        return $levelElements;
519
    }
520
521
    /**
522
     * Returns all path element strings found for the given path $levelData.
523
     *
524
     * @param array $levelData
525
     *
526
     * @return string[]
527
     */
528
    protected function extractPathElements(array $levelData)
529
    {
530
        $elements = [];
531
532
        if (isset($levelData['translations']['always-available'])) {
533
            // NOP entry
534
            $elements[] = $levelData['translations']['always-available'];
535
        } else {
536
            // Language(s) entry
537
            $elements = array_values($levelData['translations']);
538
        }
539
540
        return $elements;
541
    }
542
543
    /**
544
     * Generates URL aliases from the Location and Content data to the migration table.
545
     */
546 View Code Duplication
    protected function generateLocationAliases()
547
    {
548
        $tableName = static::MIGRATION_TABLE;
549
550
        if (!$this->tableExists($tableName)) {
551
            throw new RuntimeException(
552
                "Could not find main URL alias migration table '{$tableName}'. " .
553
                'Ensure that table exists (you will have to create it manually).'
554
            );
555
        }
556
557
        if (!$this->isTableEmpty($tableName)) {
558
            throw new RuntimeException("Table '{$tableName}' contains data.");
559
        }
560
561
        $this->doGenerateLocationAliases();
562
    }
563
564
    /**
565
     * Internal method for generating URL aliases.
566
     *
567
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::generateLocationAliases()
568
     */
569
    protected function doGenerateLocationAliases()
570
    {
571
        $totalLocationCount = $this->getTotalLocationCount();
572
        $totalContentCount = $this->getTotalLocationContentCount();
573
        $passCount = ceil($totalLocationCount / $this->bulkCount);
574
        $publishedAliasCount = 0;
575
576
        if ($totalLocationCount === 0) {
577
            $this->output->writeln('Could not find any Locations, nothing to generate.');
578
            $this->output->writeln('');
579
580
            return;
581
        }
582
583
        $queryBuilder = $this->connection->createQueryBuilder();
584
        $queryBuilder
585
            ->select('node_id', 'parent_node_id', 'contentobject_id')
586
            ->from('ezcontentobject_tree')
587
            ->where($queryBuilder->expr()->neq('node_id', 1))
588
            ->orderBy('depth', 'ASC')
589
            ->orderBy('node_id', 'ASC');
590
591
        $this->output->writeln(
592
            "Publishing URL aliases for {$totalLocationCount} Location(s) " .
593
            "with {$totalContentCount} Content item(s) in all languages."
594
        );
595
596
        $progressBar = $this->getProgressBar($totalLocationCount);
597
        $progressBar->start();
598
599
        for ($pass = 0; $pass <= $passCount; ++$pass) {
600
            $rows = $this->loadLocationData($queryBuilder, $pass);
601
602
            foreach ($rows as $row) {
603
                $publishedAliasCount += $this->publishAliases(
604
                    $row['node_id'],
605
                    $row['parent_node_id'],
606
                    $row['contentobject_id']
607
                );
608
            }
609
610
            $progressBar->advance(count($rows));
611
        }
612
613
        $progressBar->finish();
614
615
        $this->output->writeln('');
616
        $this->output->writeln("Done. Published {$publishedAliasCount} URL alias(es).");
617
        $this->output->writeln('');
618
    }
619
620
    /**
621
     * Backups global URL aliases the custom URL alias backup table.
622
     */
623 View Code Duplication
    protected function backupGlobalAliases()
624
    {
625
        $table = static::GLOBAL_ALIAS_BACKUP_TABLE;
626
627
        if (!$this->tableExists($table)) {
628
            $this->createGlobalUrlAliasBackupTable();
629
        }
630
631
        if (!$this->isTableEmpty($table)) {
632
            throw new RuntimeException(
633
                "Table '{$table}' contains data. " .
634
                "Ensure it's empty or non-existent (it will be automatically created)."
635
            );
636
        }
637
638
        $this->doBackupGlobalAliases();
639
    }
640
641
    /**
642
     * Internal method for backing up global URL aliases.
643
     *
644
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::backupGlobalAliases()
645
     */
646
    protected function doBackupGlobalAliases()
647
    {
648
        $aliases = $this->urlAliasHandler->listGlobalURLAliases();
649
        $totalCount = count($aliases);
650
        $pathCount = 0;
651
652
        if ($totalCount === 0) {
653
            $this->output->writeln('Could not find any global URL aliases, nothing to backup.');
654
            $this->output->writeln('');
655
656
            return;
657
        }
658
659
        $this->output->writeln("Backing up {$totalCount} global URL aliases.");
660
661
        $progressBar = $this->getProgressBar($totalCount);
662
        $progressBar->start();
663
664
        foreach ($aliases as $alias) {
665
            $pathCount += $this->storeGlobalAlias($alias);
666
            $progressBar->advance();
667
        }
668
669
        $progressBar->finish();
670
671
        $this->output->writeln('');
672
        $this->output->writeln(
673
            "Done. Backed up {$totalCount} global URL alias(es) " .
674
            "with {$pathCount} path(s)."
675
        );
676
        $this->output->writeln('');
677
    }
678
679
    /**
680
     * Stores given global URL $alias to the global URL alias backup table.
681
     *
682
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias $alias
683
     *
684
     * @return int
685
     */
686
    protected function storeGlobalAlias(UrlAlias $alias)
687
    {
688
        $paths = $this->combinePaths($alias->pathData);
689
690 View Code Duplication
        foreach ($paths as $path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
691
            $this->storeGlobalAliasPath(
692
                $alias->destination,
693
                $path,
694
                reset($alias->languageCodes),
695
                $alias->alwaysAvailable,
696
                $alias->forward
697
            );
698
        }
699
700
        return count($paths);
701
    }
702
703
    /**
704
     * Stores global URL alias data for $path to the backup table.
705
     *
706
     * @param string $resource
707
     * @param string $path
708
     * @param string $languageCode
709
     * @param bool $alwaysAvailable
710
     * @param bool $forwarding
711
     */
712 View Code Duplication
    protected function storeGlobalAliasPath(
713
        $resource,
714
        $path,
715
        $languageCode,
716
        $alwaysAvailable,
717
        $forwarding
718
    ) {
719
        $queryBuilder = $this->connection->createQueryBuilder();
720
721
        $queryBuilder->insert(static::GLOBAL_ALIAS_BACKUP_TABLE);
722
        $queryBuilder->values(
723
            [
724
                'id' => '?',
725
                'resource' => '?',
726
                'path' => '?',
727
                'language_code' => '?',
728
                'always_available' => '?',
729
                'forwarding' => '?',
730
            ]
731
        );
732
        $queryBuilder->setParameter(0, 0);
733
        $queryBuilder->setParameter(1, 'module:' . $resource);
734
        $queryBuilder->setParameter(2, $path);
735
        $queryBuilder->setParameter(3, $languageCode);
736
        $queryBuilder->setParameter(4, (int)$alwaysAvailable);
737
        $queryBuilder->setParameter(5, (int)$forwarding);
738
739
        $queryBuilder->execute();
740
    }
741
742
    /**
743
     * Restores global URL aliases from the backup table.
744
     */
745 View Code Duplication
    protected function restoreGlobalAliases()
746
    {
747
        $table = static::MIGRATION_TABLE;
748
        $backupTable = static::GLOBAL_ALIAS_BACKUP_TABLE;
749
750
        if (!$this->tableExists($table)) {
751
            throw new RuntimeException(
752
                "Could not find main URL alias migration table '{$table}'. " .
753
                'Ensure that table exists (you will have to create it manually).'
754
            );
755
        }
756
757
        if (!$this->tableExists($backupTable)) {
758
            throw new RuntimeException(
759
                "Could not find global URL alias backup table '$backupTable'. " .
760
                "Ensure that table is created by 'backup-global' action."
761
            );
762
        }
763
764
        $this->doRestoreGlobalAliases();
765
    }
766
767
    /**
768
     * Restores global URL aliases from the backup table.
769
     *
770
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::restoreGlobalAliases()
771
     */
772 View Code Duplication
    protected function doRestoreGlobalAliases()
773
    {
774
        $totalCount = $this->getTotalBackupCount(static::GLOBAL_ALIAS_BACKUP_TABLE);
775
        $passCount = ceil($totalCount / $this->bulkCount);
776
        $createdAliasCount = 0;
777
        $conflictCount = 0;
778
779
        if ($totalCount === 0) {
780
            $this->output->writeln(
781
                'Could not find any backed up global URL aliases, nothing to restore.'
782
            );
783
            $this->output->writeln('');
784
785
            return;
786
        }
787
788
        $queryBuilder = $this->connection->createQueryBuilder();
789
        $queryBuilder
790
            ->select('*')
791
            ->from(static::GLOBAL_ALIAS_BACKUP_TABLE)
792
            ->orderBy('id', 'ASC');
793
794
        $this->output->writeln("Restoring {$totalCount} custom URL alias(es).");
795
796
        $progressBar = $this->getProgressBar($totalCount);
797
        $progressBar->start();
798
799
        for ($pass = 0; $pass <= $passCount; ++$pass) {
800
            $rows = $this->loadPassData($queryBuilder, $pass);
801
802
            foreach ($rows as $row) {
803
                try {
804
                    $this->setMigrationTable();
805
                    $this->urlAliasHandler->createGlobalUrlAlias(
806
                        $row['resource'],
807
                        $row['path'],
808
                        (bool)$row['forwarding'],
809
                        $row['language_code'],
810
                        (bool)$row['always_available']
811
                    );
812
                    $createdAliasCount += 1;
813
                    $this->setDefaultTable();
814
                } catch (ForbiddenException $e) {
815
                    $conflictCount += 1;
816
                } catch (Exception $e) {
817
                    $this->setDefaultTable();
818
                    throw $e;
819
                }
820
            }
821
822
            $progressBar->advance(count($rows));
823
        }
824
825
        $progressBar->finish();
826
827
        $this->output->writeln('');
828
        $this->output->writeln(
829
            "Done. Restored {$createdAliasCount} custom URL alias(es) " .
830
            "with {$conflictCount} conflict(s)."
831
        );
832
        $this->output->writeln('');
833
    }
834
835
    /**
836
     * Publishes URL aliases in all languages for the given parameters.
837
     *
838
     * @throws \Exception
839
     *
840
     * @param int|string $locationId
841
     * @param int|string $parentLocationId
842
     * @param int|string $contentId
843
     *
844
     * @return int
845
     */
846
    protected function publishAliases($locationId, $parentLocationId, $contentId)
847
    {
848
        $content = $this->contentService->loadContent($contentId);
849
850
        $urlAliasNames = $this->nameSchemaResolver->resolveUrlAliasSchema($content);
851
852
        foreach ($urlAliasNames as $languageCode => $name) {
853
            try {
854
                $this->setMigrationTable();
855
                $this->urlAliasHandler->publishUrlAliasForLocation(
856
                    $locationId,
857
                    $parentLocationId,
858
                    $name,
859
                    $languageCode,
860
                    $content->contentInfo->alwaysAvailable
861
                );
862
                $this->setDefaultTable();
863
            } catch (Exception $e) {
864
                $this->setDefaultTable();
865
                throw $e;
866
            }
867
        }
868
869
        return count($urlAliasNames);
870
    }
871
872
    /**
873
     * Loads Location data for the given $pass.
874
     *
875
     * @param \Doctrine\DBAL\Query\QueryBuilder $queryBuilder
876
     * @param int $pass
877
     *
878
     * @return array
879
     */
880 View Code Duplication
    protected function loadLocationData(QueryBuilder $queryBuilder, $pass)
881
    {
882
        $queryBuilder->setFirstResult($pass * $this->bulkCount);
883
        $queryBuilder->setMaxResults($this->bulkCount);
884
885
        $statement = $queryBuilder->execute();
886
887
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
888
889
        return $rows;
890
    }
891
892
    /**
893
     * Returns total number of Locations in the database.
894
     *
895
     * The number excludes absolute root Location, which does not have an URL alias.
896
     */
897 View Code Duplication
    protected function getTotalLocationCount()
898
    {
899
        $platform = $this->connection->getDatabasePlatform();
900
901
        $queryBuilder = $this->connection->createQueryBuilder();
902
        $queryBuilder
903
            ->select($platform->getCountExpression('node_id'))
904
            ->from('ezcontentobject_tree')
905
            ->where(
906
                $queryBuilder->expr()->neq(
907
                    'node_id',
908
                    UrlAliasHandler::ROOT_LOCATION_ID
909
                )
910
            );
911
912
        return $queryBuilder->execute()->fetchColumn();
913
    }
914
915
    /**
916
     * Returns total number of Content objects having a Location in the database.
917
     *
918
     * The number excludes absolute root Location, which does not have an URL alias.
919
     */
920 View Code Duplication
    protected function getTotalLocationContentCount()
921
    {
922
        $platform = $this->connection->getDatabasePlatform();
923
924
        $queryBuilder = $this->connection->createQueryBuilder();
925
        $queryBuilder
926
            ->select($platform->getCountExpression('DISTINCT contentobject_id'))
927
            ->from('ezcontentobject_tree')
928
            ->where(
929
                $queryBuilder->expr()->neq(
930
                    'node_id',
931
                    UrlAliasHandler::ROOT_LOCATION_ID
932
                )
933
            );
934
935
        return $queryBuilder->execute()->fetchColumn();
936
    }
937
938
    /**
939
     * Return the number of rows in the given $table (on ID column).
940
     *
941
     * @param string $table
942
     *
943
     * @return int
944
     */
945 View Code Duplication
    protected function getTotalBackupCount($table)
946
    {
947
        $platform = $this->connection->getDatabasePlatform();
948
949
        $queryBuilder = $this->connection->createQueryBuilder();
950
        $queryBuilder
951
            ->select($platform->getCountExpression('id'))
952
            ->from($table);
953
954
        return (int)$queryBuilder->execute()->fetchColumn();
955
    }
956
957
    /**
958
     * Creates database table for custom Location URL alias backup.
959
     */
960 View Code Duplication
    protected function createCustomLocationUrlAliasBackupTable()
961
    {
962
        $schema = new Schema();
963
964
        $table = $schema->createTable(static::CUSTOM_ALIAS_BACKUP_TABLE);
965
966
        $table->addColumn('id', 'integer', ['autoincrement' => true]);
967
        $table->addColumn('location_id', 'integer');
968
        $table->addColumn('path', 'text');
969
        $table->addColumn('language_code', 'string');
970
        $table->addColumn('always_available', 'integer');
971
        $table->addColumn('forwarding', 'integer');
972
        $table->setPrimaryKey(['id']);
973
974
        $queries = $schema->toSql($this->connection->getDatabasePlatform());
975
976
        foreach ($queries as $query) {
977
            $this->connection->exec($query);
978
        }
979
    }
980
981
    /**
982
     * Creates database table for custom URL alias backup.
983
     */
984 View Code Duplication
    protected function createGlobalUrlAliasBackupTable()
985
    {
986
        $schema = new Schema();
987
988
        $table = $schema->createTable(static::GLOBAL_ALIAS_BACKUP_TABLE);
989
990
        $table->addColumn('id', 'integer', ['autoincrement' => true]);
991
        $table->addColumn('resource', 'text');
992
        $table->addColumn('path', 'text');
993
        $table->addColumn('language_code', 'string');
994
        $table->addColumn('always_available', 'integer');
995
        $table->addColumn('forwarding', 'integer');
996
        $table->setPrimaryKey(['id']);
997
998
        $queries = $schema->toSql($this->connection->getDatabasePlatform());
999
1000
        foreach ($queries as $query) {
1001
            $this->connection->exec($query);
1002
        }
1003
    }
1004
1005
    /**
1006
     * Checks if database table $name exists.
1007
     *
1008
     * @param string $name
1009
     *
1010
     * @return bool
1011
     */
1012
    protected function tableExists($name)
1013
    {
1014
        return $this->connection->getSchemaManager()->tablesExist([$name]);
1015
    }
1016
1017
    /**
1018
     * Checks if database table $name is empty.
1019
     *
1020
     * @param string $name
1021
     *
1022
     * @return bool
1023
     */
1024 View Code Duplication
    protected function isTableEmpty($name)
1025
    {
1026
        $queryBuilder = $this->connection->createQueryBuilder();
1027
        $queryBuilder
1028
            ->select($this->connection->getDatabasePlatform()->getCountExpression('*'))
1029
            ->from($name);
1030
1031
        $count = $queryBuilder->execute()->fetchColumn();
1032
1033
        return $count == 0;
1034
    }
1035
1036
    /**
1037
     * Returns configured progress bar helper.
1038
     *
1039
     * @param int $maxSteps
1040
     *
1041
     * @return \Symfony\Component\Console\Helper\ProgressBar
1042
     */
1043
    protected function getProgressBar($maxSteps)
1044
    {
1045
        $progressBar = new ProgressBar($this->output, $maxSteps);
1046
        $progressBar->setFormat(
1047
            ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'
1048
        );
1049
1050
        return $progressBar;
1051
    }
1052
}
1053