Completed
Push — EZP-26146-location-swap-urlali... ( 8ddb19...fa30fa )
by
unknown
28:50
created

RegenerateUrlAliasesCommand::storeGlobalAlias()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 9
Ratio 56.25 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 1
dl 9
loc 16
rs 9.4285
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
 * @version //autogentag//
10
 */
11
namespace eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage;
12
13
use eZ\Publish\API\Repository\Exceptions\ForbiddenException;
14
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway as UrlAliasGateway;
15
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler as UrlAliasHandler;
16
use eZ\Publish\Core\Persistence\Legacy\Handler as LegacyStorageEngine;
17
use eZ\Publish\SPI\Persistence\Content\UrlAlias;
18
use Doctrine\DBAL\Query\QueryBuilder;
19
use Doctrine\DBAL\Schema\Schema;
20
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
21
use Symfony\Component\Console\Input\InputArgument;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Helper\ProgressBar;
24
use Symfony\Component\Console\Output\OutputInterface;
25
use RuntimeException;
26
use Exception;
27
use PDO;
28
29
class RegenerateUrlAliasesCommand extends ContainerAwareCommand
30
{
31
    const MIGRATION_TABLE = '__migration_ezurlalias_ml';
32
    const CUSTOM_ALIAS_BACKUP_TABLE = '__migration_backup_custom_alias';
33
    const GLOBAL_ALIAS_BACKUP_TABLE = '__migration_backup_global_alias';
34
35
    /**
36
     * @var \eZ\Publish\API\Repository\ContentService
37
     */
38
    protected $contentService;
39
40
    /**
41
     * @var \eZ\Publish\Core\Repository\Helper\NameSchemaService
42
     */
43
    protected $nameSchemaResolver;
44
45
    /**
46
     * @var \eZ\Publish\SPI\Persistence\Content\UrlAlias\Handler
47
     */
48
    protected $urlAliasHandler;
49
50
    /**
51
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
52
     */
53
    protected $urlAliasGateway;
54
55
    /**
56
     * @var \Doctrine\DBAL\Connection
57
     */
58
    protected $connection;
59
60
    /**
61
     * @var int
62
     */
63
    protected $bulkCount;
64
65
    /**
66
     * @var \Symfony\Component\Console\Output\OutputInterface
67
     */
68
    protected $output;
69
70
    protected $actionSet = [
71
        'full' => true,
72
        'autogenerate' => true,
73
        'backup-custom' => true,
74
        'restore-custom' => true,
75
        'backup-global' => true,
76
        'restore-global' => true,
77
    ];
78
79
    protected function configure()
80
    {
81
        $this
82
            ->setName('ezpublish:regenerate:legacy_storage_url_aliases')
83
            ->setDescription('Updates sort keys in configured Legacy Storage database')
84
            ->addArgument(
85
                'action',
86
                InputArgument::REQUIRED,
87
                'Action to perform, one of: full, autogenerate, backup-custom, restore-custom, backup-global, restore-global'
88
            )
89
            ->addArgument(
90
                'bulk-count',
91
                InputArgument::OPTIONAL,
92
                'Number of items (Locations, URL aliases) processed at once',
93
                50
94
            )
95
            ->setHelp(
96
                <<<EOT
97
The command <info>%command.name%</info> regenerates URL aliases for Locations
98
and migrates existing custom Location and global URL aliases to a separate database table. Separate
99
table must be named '__migration_ezurlalias_ml' and should be created manually to be identical (but
100
empty) as the existing table 'ezurlalias_ml' before the command is executed.
101
102
After the script finishes, to complete migration the table should be renamed to 'ezurlalias_ml'
103
manually.
104
105
Using available options for 'action' argument, you can backup custom Location and global URL aliases
106
separately and inspect them before restoring them to the migration table. They will be stored in
107
backup tables named '__migration_backup_custom_alias' and '__migration_backup_global_alias' (created
108
automatically).
109
110
It is also possible to skip custom Location and global URL aliases altogether and regenerate only
111
automatically created URL aliases for Locations (use 'autogenerate' action to achieve this).
112
113
<error>During the script execution the database should not be modified.</error>
114
115
Since this script can potentially run for a very long time, to avoid memory exhaustion run it in
116
production environment using <info>--env=prod</info> switch.
117
118
EOT
119
            );
120
    }
121
122
    protected function execute(InputInterface $input, OutputInterface $output)
123
    {
124
        $this->checkStorage();
125
        $this->prepareDependencies($output);
126
127
        $action = $input->getArgument('action');
128
        $this->bulkCount = $input->getArgument('bulk-count');
129
130
        if (!isset($this->actionSet[$action])) {
131
            throw new RuntimeException("Action '{$action}' is not supported");
132
        }
133
134
        if ($action === 'full' || $action === 'backup-custom') {
135
            $this->backupCustomLocationAliases();
136
        }
137
138
        if ($action === 'full' || $action === 'backup-global') {
139
            $this->backupGlobalAliases();
140
        }
141
142
        if ($action === 'full' || $action === 'autogenerate') {
143
            $this->generateLocationAliases();
144
        }
145
146
        if ($action === 'full' || $action === 'restore-custom') {
147
            $this->restoreCustomLocationAliases();
148
        }
149
150
        if ($action === 'full' || $action === 'restore-global') {
151
            $this->restoreGlobalAliases();
152
        }
153
    }
154
155
    /**
156
     * Checks that configured storage engine is Legacy Storage Engine.
157
     */
158
    protected function checkStorage()
159
    {
160
        $storageEngine = $this->getContainer()->get('ezpublish.api.storage_engine');
161
162
        if (!$storageEngine instanceof LegacyStorageEngine) {
163
            throw new RuntimeException(
164
                'Expected to find Legacy Storage Engine but found something else.'
165
            );
166
        }
167
    }
168
169
    /**
170
     * Prepares dependencies used by the command.
171
     *
172
     * @param \Symfony\Component\Console\Output\OutputInterface $output
173
     */
174
    protected function prepareDependencies(OutputInterface $output)
175
    {
176
        /** @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler $databaseHandler */
177
        $databaseHandler = $this->getContainer()->get('ezpublish.connection');
178
        /** @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway */
179
        $gateway = $this->getContainer()->get('ezpublish.persistence.legacy.url_alias.gateway');
180
        /** @var \eZ\Publish\SPI\Persistence\Handler $persistenceHandler */
181
        $persistenceHandler = $this->getContainer()->get('ezpublish.api.persistence_handler');
182
        /** @var \eZ\Publish\Core\Repository\Repository $innerRepository */
183
        $innerRepository = $this->getContainer()->get('ezpublish.api.inner_repository');
184
        /** @var \eZ\Publish\API\Repository\Repository $repository */
185
        $repository = $this->getContainer()->get('ezpublish.api.repository');
186
187
        $administratorUser = $repository->getUserService()->loadUser(14);
188
        $repository->getPermissionResolver()->setCurrentUserReference($administratorUser);
189
190
        $this->contentService = $repository->getContentService();
191
        $this->nameSchemaResolver = $innerRepository->getNameSchemaService();
192
        $this->urlAliasHandler = $persistenceHandler->urlAliasHandler();
193
        $this->urlAliasGateway = $gateway;
194
        $this->connection = $databaseHandler->getConnection();
195
        $this->output = $output;
196
    }
197
198
    /**
199
     * Sets storage gateway to the default table.
200
     *
201
     * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway::TABLE
202
     */
203
    protected function setDefaultTable()
204
    {
205
        $this->urlAliasGateway->setTable(UrlAliasGateway::TABLE);
206
    }
207
208
    /**
209
     * Sets storage gateway to the migration table.
210
     *
211
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::MIGRATION_TABLE
212
     */
213
    protected function setMigrationTable()
214
    {
215
        $this->urlAliasGateway->setTable(static::MIGRATION_TABLE);
216
    }
217
218
    /**
219
     * Backups custom Location URL aliases the custom URL alias backup table.
220
     */
221 View Code Duplication
    protected function backupCustomLocationAliases()
222
    {
223
        $table = static::CUSTOM_ALIAS_BACKUP_TABLE;
224
225
        if (!$this->tableExists($table)) {
226
            $this->createCustomLocationUrlAliasBackupTable();
227
        }
228
229
        if (!$this->isTableEmpty($table)) {
230
            throw new RuntimeException(
231
                "Table '{$table}' contains data. " .
232
                "Ensure it's empty or non-existent (it will be automatically created)."
233
            );
234
        }
235
236
        $this->doBackupCustomLocationAliases();
237
    }
238
239
    /**
240
     * Internal method for backing up custom Location URL aliases.
241
     *
242
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::backupCustomLocationAliases()
243
     */
244
    protected function doBackupCustomLocationAliases()
245
    {
246
        $totalCount = $this->getTotalLocationCount();
247
        $passCount = ceil($totalCount / $this->bulkCount);
248
        $customAliasCount = 0;
249
        $customAliasPathCount = 0;
250
251
        if ($totalCount === 0) {
252
            $this->output->writeln('Could not find any Locations, nothing to backup.');
253
            $this->output->writeln('');
254
            return;
255
        }
256
257
        $queryBuilder = $this->connection->createQueryBuilder();
258
        $queryBuilder
259
            ->select('node_id', 'parent_node_id', 'contentobject_id')
260
            ->from('ezcontentobject_tree')
261
            ->where($queryBuilder->expr()->neq('node_id', 1))
262
            ->orderBy('depth', 'ASC')
263
            ->orderBy('node_id', 'ASC');
264
265
        $this->output->writeln("Backing up custom URL alias(es) for {$totalCount} Location(s).");
266
267
        $progressBar = $this->getProgressBar($totalCount);
268
        $progressBar->start();
269
270
        for ($pass = 0; $pass <= $passCount; ++$pass) {
271
            $rows = $this->loadLocationData($queryBuilder, $pass);
272
273
            foreach ($rows as $row) {
274
                $customAliases = $this->urlAliasHandler->listURLAliasesForLocation(
275
                    $row['node_id'],
276
                    true
277
                );
278
279
                $customAliasCount += count($customAliases);
280
                $customAliasPathCount += $this->storeCustomAliases($customAliases);
281
282
            }
283
284
            $progressBar->advance(count($rows));
285
        }
286
287
        $progressBar->finish();
288
289
        $this->output->writeln('');
290
        $this->output->writeln(
291
            "Done. Backed up {$customAliasCount} custom URL alias(es) " .
292
            "with {$customAliasPathCount} path(s)."
293
        );
294
        $this->output->writeln('');
295
    }
296
297
    /**
298
     * Restores custom Location URL aliases from the backup table.
299
     */
300 View Code Duplication
    protected function restoreCustomLocationAliases()
301
    {
302
        $mainTable = static::MIGRATION_TABLE;
303
        $backupTable = static::CUSTOM_ALIAS_BACKUP_TABLE;
304
305
        if (!$this->tableExists($mainTable)) {
306
            throw new RuntimeException(
307
                "Could not find main URL alias migration table '{$mainTable}'. " .
308
                'Ensure that table exists (you will have to create it manually).'
309
            );
310
        }
311
312
        if (!$this->tableExists($backupTable)) {
313
            throw new RuntimeException(
314
                "Could not find custom Location URL alias backup table '{$backupTable}'. " .
315
                "Ensure that table is created by 'backup-custom' action."
316
            );
317
        }
318
319
        $this->doRestoreCustomLocationAliases();
320
    }
321
322
    /**
323
     * Restores custom Location URL aliases from the backup table.
324
     *
325
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::restoreCustomLocationAliases()
326
     */
327 View Code Duplication
    protected function doRestoreCustomLocationAliases()
328
    {
329
        $totalCount = $this->getTotalBackupCount(static::CUSTOM_ALIAS_BACKUP_TABLE);
330
        $passCount = ceil($totalCount / $this->bulkCount);
331
        $createdAliasCount = 0;
332
        $conflictCount = 0;
333
334
        if ($totalCount === 0) {
335
            $this->output->writeln(
336
                'Could not find any backed up custom Location URL aliases, nothing to restore.'
337
            );
338
            $this->output->writeln('');
339
            return;
340
        }
341
342
        $queryBuilder = $this->connection->createQueryBuilder();
343
        $queryBuilder
344
            ->select('*')
345
            ->from(static::CUSTOM_ALIAS_BACKUP_TABLE)
346
            ->orderBy('id', 'ASC');
347
348
        $this->output->writeln("Restoring {$totalCount} custom URL alias(es).");
349
350
        $progressBar = $this->getProgressBar($totalCount);
351
        $progressBar->start();
352
353
        for ($pass = 0; $pass <= $passCount; ++$pass) {
354
            $rows = $this->loadPassData($queryBuilder, $pass);
355
356
            foreach ($rows as $row) {
357
                try {
358
                    $this->setMigrationTable();
359
                    $this->urlAliasHandler->createCustomUrlAlias(
360
                        $row['location_id'],
361
                        $row['path'],
362
                        (bool)$row['forwarding'],
363
                        $row['language_code'],
364
                        (bool)$row['always_available']
365
                    );
366
                    $createdAliasCount += 1;
367
                    $this->setDefaultTable();
368
                } catch (ForbiddenException $e) {
369
                    $conflictCount += 1;
370
                } catch (Exception $e) {
371
                    $this->setDefaultTable();
372
                    throw $e;
373
                }
374
            }
375
376
            $progressBar->advance(count($rows));
377
        }
378
379
        $progressBar->finish();
380
381
        $this->output->writeln('');
382
        $this->output->writeln(
383
            "Done. Restored {$createdAliasCount} custom URL alias(es) " .
384
            "with {$conflictCount} conflict(s)."
385
        );
386
        $this->output->writeln('');
387
    }
388
389
    /**
390
     * Loads Location data for the given $pass.
391
     *
392
     * @param \Doctrine\DBAL\Query\QueryBuilder $queryBuilder
393
     * @param int $pass
394
     *
395
     * @return array
396
     */
397 View Code Duplication
    protected function loadPassData(QueryBuilder $queryBuilder, $pass)
398
    {
399
        $queryBuilder->setFirstResult($pass * $this->bulkCount);
400
        $queryBuilder->setMaxResults($this->bulkCount);
401
402
        $statement = $queryBuilder->execute();
403
404
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
405
406
        return $rows;
407
    }
408
409
    /**
410
     * Stores given custom $aliases to the custom alias backup table.
411
     *
412
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $aliases
413
     *
414
     * @return int
415
     */
416
    protected function storeCustomAliases(array $aliases)
417
    {
418
        $pathCount = 0;
419
420
        foreach ($aliases as $alias) {
421
            $paths = $this->combinePaths($alias->pathData);
422
            $pathCount += count($paths);
423
424 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...
425
                $this->storeCustomAliasPath(
426
                    $alias->destination,
427
                    $path,
428
                    reset($alias->languageCodes),
429
                    $alias->alwaysAvailable,
430
                    $alias->forward
431
                );
432
            }
433
        }
434
435
        return $pathCount;
436
    }
437
438
    /**
439
     * Stores custom URL alias data for $path to the backup table.
440
     *
441
     * @param int $locationId
442
     * @param string $path
443
     * @param string $languageCode
444
     * @param bool $alwaysAvailable
445
     * @param bool $forwarding
446
     */
447 View Code Duplication
    protected function storeCustomAliasPath(
448
        $locationId,
449
        $path,
450
        $languageCode,
451
        $alwaysAvailable,
452
        $forwarding
453
    ) {
454
        $queryBuilder = $this->connection->createQueryBuilder();
455
456
        $queryBuilder->insert(static::CUSTOM_ALIAS_BACKUP_TABLE);
457
        $queryBuilder->values(
458
            [
459
                'id' => '?',
460
                'location_id' => '?',
461
                'path' => '?',
462
                'language_code' => '?',
463
                'always_available' => '?',
464
                'forwarding' => '?',
465
            ]
466
        );
467
        $queryBuilder->setParameter(0, 0);
468
        $queryBuilder->setParameter(1, $locationId);
469
        $queryBuilder->setParameter(2, $path);
470
        $queryBuilder->setParameter(3, $languageCode);
471
        $queryBuilder->setParameter(4, (int)$alwaysAvailable);
472
        $queryBuilder->setParameter(5, (int)$forwarding);
473
474
        $queryBuilder->execute();
475
    }
476
477
    /**
478
     * Combines path data to an array of URL alias paths.
479
     *
480
     * Explanation:
481
     *
482
     * Custom Location and global URL aliases can generate NOP entries, which can be taken over by
483
     * the autogenerated aliases. When multiple languages exists for the Location that took over,
484
     * multiple entries with the same link will exist on the same level. In that case it will not be
485
     * possible to reliably reconstruct what was the path for the original custom alias. For that
486
     * reason we combine path data to get all possible path combinations.
487
     *
488
     * Note: it could happen that original NOP entry was historized after being taken over by the
489
     * autogenerated alias. So to be complete this would have to take into account history entries
490
     * as well, but at the moment we lack API to do that.
491
     *
492
     * Proper solution of this problem would be introducing separate database table to store
493
     * custom/global URL alias data.
494
     *
495
     * @see https://jira.ez.no/browse/EZP-20777
496
     *
497
     * @param array $pathData
498
     *
499
     * @return string[]
500
     */
501
    protected function combinePaths(array $pathData)
502
    {
503
        $paths = [];
504
        $levelData = array_shift($pathData);
505
        $levelElements = $this->extractPathElements($levelData);
506
507
        if (!empty($pathData)) {
508
            $nextElements = $this->combinePaths($pathData);
509
510
            foreach ($levelElements as $element1) {
511
                foreach ($nextElements as $element2) {
512
                    $paths[] = $element1 . '/' . $element2;
513
                }
514
            }
515
516
            return $paths;
517
        }
518
519
        return $levelElements;
520
    }
521
522
    /**
523
     * Returns all path element strings found for the given path $levelData.
524
     *
525
     * @param array $levelData
526
     *
527
     * @return string[]
528
     */
529
    protected function extractPathElements(array $levelData)
530
    {
531
        $elements = [];
532
533
        if (isset($levelData['translations']['always-available'])) {
534
            // NOP entry
535
            $elements[] = $levelData['translations']['always-available'];
536
        } else {
537
            // Language(s) entry
538
            $elements = array_values($levelData['translations']);
539
        }
540
541
        return $elements;
542
    }
543
544
    /**
545
     * Generates URL aliases from the Location and Content data to the migration table.
546
     */
547 View Code Duplication
    protected function generateLocationAliases()
548
    {
549
        $tableName = static::MIGRATION_TABLE;
550
551
        if (!$this->tableExists($tableName)) {
552
            throw new RuntimeException(
553
                "Could not find main URL alias migration table '{$tableName}'. " .
554
                'Ensure that table exists (you will have to create it manually).'
555
            );
556
        }
557
558
        if (!$this->isTableEmpty($tableName)) {
559
            throw new RuntimeException("Table '{$tableName}' contains data.");
560
        }
561
562
        $this->doGenerateLocationAliases();
563
    }
564
565
    /**
566
     * Internal method for generating URL aliases.
567
     *
568
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::generateLocationAliases()
569
     */
570
    protected function doGenerateLocationAliases()
571
    {
572
        $totalLocationCount = $this->getTotalLocationCount();
573
        $totalContentCount = $this->getTotalLocationContentCount();
574
        $passCount = ceil($totalLocationCount / $this->bulkCount);
575
        $publishedAliasCount = 0;
576
577
        if ($totalLocationCount === 0) {
578
            $this->output->writeln('Could not find any Locations, nothing to generate.');
579
            $this->output->writeln('');
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
            return;
656
        }
657
658
        $this->output->writeln("Backing up {$totalCount} global URL aliases.");
659
660
        $progressBar = $this->getProgressBar($totalCount);
661
        $progressBar->start();
662
663
        foreach ($aliases as $alias) {
664
            $pathCount += $this->storeGlobalAlias($alias);
665
            $progressBar->advance();
666
        }
667
668
        $progressBar->finish();
669
670
        $this->output->writeln('');
671
        $this->output->writeln(
672
            "Done. Backed up {$totalCount} global URL alias(es) " .
673
            "with {$pathCount} path(s)."
674
        );
675
        $this->output->writeln('');
676
    }
677
678
    /**
679
     * Stores given global URL $alias to the global URL alias backup table.
680
     *
681
     * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias $alias
682
     *
683
     * @return int
684
     */
685
    protected function storeGlobalAlias(UrlAlias $alias)
686
    {
687
        $paths = $this->combinePaths($alias->pathData);
688
689 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...
690
            $this->storeGlobalAliasPath(
691
                $alias->destination,
692
                $path,
693
                reset($alias->languageCodes),
694
                $alias->alwaysAvailable,
695
                $alias->forward
696
            );
697
        }
698
699
        return count($paths);
700
    }
701
702
    /**
703
     * Stores global URL alias data for $path to the backup table.
704
     *
705
     * @param string $resource
706
     * @param string $path
707
     * @param string $languageCode
708
     * @param bool $alwaysAvailable
709
     * @param bool $forwarding
710
     */
711 View Code Duplication
    protected function storeGlobalAliasPath(
712
        $resource,
713
        $path,
714
        $languageCode,
715
        $alwaysAvailable,
716
        $forwarding
717
    ) {
718
        $queryBuilder = $this->connection->createQueryBuilder();
719
720
        $queryBuilder->insert(static::GLOBAL_ALIAS_BACKUP_TABLE);
721
        $queryBuilder->values(
722
            [
723
                'id' => '?',
724
                'resource' => '?',
725
                'path' => '?',
726
                'language_code' => '?',
727
                'always_available' => '?',
728
                'forwarding' => '?',
729
            ]
730
        );
731
        $queryBuilder->setParameter(0, 0);
732
        $queryBuilder->setParameter(1, 'module:' . $resource);
733
        $queryBuilder->setParameter(2, $path);
734
        $queryBuilder->setParameter(3, $languageCode);
735
        $queryBuilder->setParameter(4, (int)$alwaysAvailable);
736
        $queryBuilder->setParameter(5, (int)$forwarding);
737
738
        $queryBuilder->execute();
739
    }
740
741
    /**
742
     * Restores global URL aliases from the backup table.
743
     */
744 View Code Duplication
    protected function restoreGlobalAliases()
745
    {
746
        $table = static::MIGRATION_TABLE;
747
        $backupTable = static::GLOBAL_ALIAS_BACKUP_TABLE;
748
749
        if (!$this->tableExists($table)) {
750
            throw new RuntimeException(
751
                "Could not find main URL alias migration table '{$table}'. " .
752
                'Ensure that table exists (you will have to create it manually).'
753
            );
754
        }
755
756
        if (!$this->tableExists($backupTable)) {
757
            throw new RuntimeException(
758
                "Could not find global URL alias backup table '$backupTable'. " .
759
                "Ensure that table is created by 'backup-global' action."
760
            );
761
        }
762
763
        $this->doRestoreGlobalAliases();
764
    }
765
766
    /**
767
     * Restores global URL aliases from the backup table.
768
     *
769
     * @see \eZ\Bundle\EzPublishMigrationBundle\Command\LegacyStorage\RegenerateUrlAliasesCommand::restoreGlobalAliases()
770
     */
771 View Code Duplication
    protected function doRestoreGlobalAliases()
772
    {
773
        $totalCount = $this->getTotalBackupCount(static::GLOBAL_ALIAS_BACKUP_TABLE);
774
        $passCount = ceil($totalCount / $this->bulkCount);
775
        $createdAliasCount = 0;
776
        $conflictCount = 0;
777
778
        if ($totalCount === 0) {
779
            $this->output->writeln(
780
                'Could not find any backed up global URL aliases, nothing to restore.'
781
            );
782
            $this->output->writeln('');
783
            return;
784
        }
785
786
        $queryBuilder = $this->connection->createQueryBuilder();
787
        $queryBuilder
788
            ->select('*')
789
            ->from(static::GLOBAL_ALIAS_BACKUP_TABLE)
790
            ->orderBy('id', 'ASC');
791
792
        $this->output->writeln("Restoring {$totalCount} custom URL alias(es).");
793
794
        $progressBar = $this->getProgressBar($totalCount);
795
        $progressBar->start();
796
797
        for ($pass = 0; $pass <= $passCount; ++$pass) {
798
            $rows = $this->loadPassData($queryBuilder, $pass);
799
800
            foreach ($rows as $row) {
801
                try {
802
                    $this->setMigrationTable();
803
                    $this->urlAliasHandler->createGlobalUrlAlias(
804
                        $row['resource'],
805
                        $row['path'],
806
                        (bool)$row['forwarding'],
807
                        $row['language_code'],
808
                        (bool)$row['always_available']
809
                    );
810
                    $createdAliasCount += 1;
811
                    $this->setDefaultTable();
812
                } catch (ForbiddenException $e) {
813
                    $conflictCount += 1;
814
                } catch (Exception $e) {
815
                    $this->setDefaultTable();
816
                    throw $e;
817
                }
818
            }
819
820
            $progressBar->advance(count($rows));
821
        }
822
823
        $progressBar->finish();
824
825
        $this->output->writeln('');
826
        $this->output->writeln(
827
            "Done. Restored {$createdAliasCount} custom URL alias(es) " .
828
            "with {$conflictCount} conflict(s)."
829
        );
830
        $this->output->writeln('');
831
    }
832
833
    /**
834
     * Publishes URL aliases in all languages for the given parameters.
835
     *
836
     * @throws \Exception
837
     *
838
     * @param int|string $locationId
839
     * @param int|string $parentLocationId
840
     * @param int|string $contentId
841
     *
842
     * @return int
843
     */
844
    protected function publishAliases($locationId, $parentLocationId, $contentId)
845
    {
846
        $content = $this->contentService->loadContent($contentId);
847
848
        $urlAliasNames = $this->nameSchemaResolver->resolveUrlAliasSchema($content);
849
850
        foreach ($urlAliasNames as $languageCode => $name) {
851
            try {
852
                $this->setMigrationTable();
853
                $this->urlAliasHandler->publishUrlAliasForLocation(
854
                    $locationId,
855
                    $parentLocationId,
856
                    $name,
857
                    $languageCode,
858
                    $content->contentInfo->alwaysAvailable
859
                );
860
                $this->setDefaultTable();
861
            } catch (Exception $e) {
862
                $this->setDefaultTable();
863
                throw $e;
864
            }
865
        }
866
867
        return count($urlAliasNames);
868
    }
869
870
    /**
871
     * Loads Location data for the given $pass.
872
     *
873
     * @param \Doctrine\DBAL\Query\QueryBuilder $queryBuilder
874
     * @param int $pass
875
     *
876
     * @return array
877
     */
878 View Code Duplication
    protected function loadLocationData(QueryBuilder $queryBuilder, $pass)
879
    {
880
        $queryBuilder->setFirstResult($pass * $this->bulkCount);
881
        $queryBuilder->setMaxResults($this->bulkCount);
882
883
        $statement = $queryBuilder->execute();
884
885
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
886
887
        return $rows;
888
    }
889
890
    /**
891
     * Returns total number of Locations in the database.
892
     *
893
     * The number excludes absolute root Location, which does not have an URL alias.
894
     */
895 View Code Duplication
    protected function getTotalLocationCount()
896
    {
897
        $platform = $this->connection->getDatabasePlatform();
898
899
        $queryBuilder = $this->connection->createQueryBuilder();
900
        $queryBuilder
901
            ->select($platform->getCountExpression('node_id'))
902
            ->from('ezcontentobject_tree')
903
            ->where(
904
                $queryBuilder->expr()->neq(
905
                    'node_id',
906
                    UrlAliasHandler::ROOT_LOCATION_ID
907
                )
908
            );
909
910
        return $queryBuilder->execute()->fetchColumn();
911
    }
912
913
    /**
914
     * Returns total number of Content objects having a Location in the database.
915
     *
916
     * The number excludes absolute root Location, which does not have an URL alias.
917
     */
918 View Code Duplication
    protected function getTotalLocationContentCount()
919
    {
920
        $platform = $this->connection->getDatabasePlatform();
921
922
        $queryBuilder = $this->connection->createQueryBuilder();
923
        $queryBuilder
924
            ->select($platform->getCountExpression('DISTINCT contentobject_id'))
925
            ->from('ezcontentobject_tree')
926
            ->where(
927
                $queryBuilder->expr()->neq(
928
                    'node_id',
929
                    UrlAliasHandler::ROOT_LOCATION_ID
930
                )
931
            );
932
933
        return $queryBuilder->execute()->fetchColumn();
934
    }
935
936
    /**
937
     * Return the number of rows in the given $table (on ID column).
938
     *
939
     * @param string $table
940
     *
941
     * @return int
942
     */
943 View Code Duplication
    protected function getTotalBackupCount($table)
944
    {
945
        $platform = $this->connection->getDatabasePlatform();
946
947
        $queryBuilder = $this->connection->createQueryBuilder();
948
        $queryBuilder
949
            ->select($platform->getCountExpression('id'))
950
            ->from($table);
951
952
        return (int)$queryBuilder->execute()->fetchColumn();
953
    }
954
955
    /**
956
     * Creates database table for custom Location URL alias backup.
957
     */
958 View Code Duplication
    protected function createCustomLocationUrlAliasBackupTable()
959
    {
960
        $schema = new Schema();
961
962
        $table = $schema->createTable(static::CUSTOM_ALIAS_BACKUP_TABLE);
963
964
        $table->addColumn('id', 'integer', ['autoincrement' => true]);
965
        $table->addColumn('location_id', 'integer');
966
        $table->addColumn('path', 'text');
967
        $table->addColumn('language_code', 'string');
968
        $table->addColumn('always_available', 'integer');
969
        $table->addColumn('forwarding', 'integer');
970
        $table->setPrimaryKey(['id']);
971
972
        $queries = $schema->toSql($this->connection->getDatabasePlatform());
973
974
        foreach ($queries as $query) {
975
            $this->connection->exec($query);
976
        }
977
    }
978
979
    /**
980
     * Creates database table for custom URL alias backup.
981
     */
982 View Code Duplication
    protected function createGlobalUrlAliasBackupTable()
983
    {
984
        $schema = new Schema();
985
986
        $table = $schema->createTable(static::GLOBAL_ALIAS_BACKUP_TABLE);
987
988
        $table->addColumn('id', 'integer', ['autoincrement' => true]);
989
        $table->addColumn('resource', 'text');
990
        $table->addColumn('path', 'text');
991
        $table->addColumn('language_code', 'string');
992
        $table->addColumn('always_available', 'integer');
993
        $table->addColumn('forwarding', 'integer');
994
        $table->setPrimaryKey(['id']);
995
996
        $queries = $schema->toSql($this->connection->getDatabasePlatform());
997
998
        foreach ($queries as $query) {
999
            $this->connection->exec($query);
1000
        }
1001
    }
1002
1003
    /**
1004
     * Checks if database table $name exists.
1005
     *
1006
     * @param string $name
1007
     *
1008
     * @return bool
1009
     */
1010
    protected function tableExists($name)
1011
    {
1012
        return $this->connection->getSchemaManager()->tablesExist([$name]);
1013
    }
1014
1015
    /**
1016
     * Checks if database table $name is empty.
1017
     *
1018
     * @param string $name
1019
     *
1020
     * @return bool
1021
     */
1022 View Code Duplication
    protected function isTableEmpty($name)
1023
    {
1024
        $queryBuilder = $this->connection->createQueryBuilder();
1025
        $queryBuilder
1026
            ->select($this->connection->getDatabasePlatform()->getCountExpression('*'))
1027
            ->from($name);
1028
1029
        $count = $queryBuilder->execute()->fetchColumn();
1030
1031
        return $count == 0;
1032
    }
1033
1034
    /**
1035
     * Returns configured progress bar helper.
1036
     *
1037
     * @param int $maxSteps
1038
     *
1039
     * @return \Symfony\Component\Console\Helper\ProgressBar
1040
     */
1041
    protected function getProgressBar($maxSteps)
1042
    {
1043
        $progressBar = new ProgressBar($this->output, $maxSteps);
1044
        $progressBar->setFormat(
1045
            ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'
1046
        );
1047
1048
        return $progressBar;
1049
    }
1050
}
1051