Completed
Push — ezp-31420-merge-up ( ec14fb...141a64 )
by
unknown
40:13 queued 27:42
created

RegenerateUrlAliasesCommand   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Importance

Changes 0
Metric Value
dl 0
loc 297
rs 10
c 0
b 0
f 0
wmc 18
lcom 2
cbo 13

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getProgressBar() 0 9 1
A __construct() 0 7 2
A configure() 0 38 1
A execute() 0 48 4
A processLocations() 0 51 4
A loadAllLocations() 0 8 1
A loadSpecificLocations() 0 10 1
A getFilteredLocationList() 0 17 1
A regenerateSystemUrlAliases() 0 25 3
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Bundle\EzPublishCoreBundle\Command;
8
9
use Exception;
10
use eZ\Publish\API\Repository\Repository;
11
use eZ\Publish\API\Repository\Values\Content\Language;
12
use eZ\Publish\API\Repository\Values\Content\Location;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\NullLogger;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Helper\ProgressBar;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\Console\Question\ConfirmationQuestion;
21
22
/**
23
 * The ezplatform:urls:regenerate-aliases Symfony command implementation.
24
 * Recreates system URL aliases for all existing Locations and cleanups corrupted URL alias nodes.
25
 */
26
class RegenerateUrlAliasesCommand extends Command
27
{
28
    const DEFAULT_ITERATION_COUNT = 1000;
29
30
    const BEFORE_RUNNING_HINTS = <<<EOT
31
<error>Before you continue:</error>
32
- Make sure to back up your database.
33
- If you are regenerating URL aliases for all Locations, take the installation offline. The database should not be modified while the script is being executed.
34
- Run this command without memory limit, because processing large numbers of Locations (e.g. 300k) can take up to 1 GB of RAM.
35
- Run this command in production environment using <info>--env=prod</info>
36
- Manually clear HTTP cache after running this command.
37
EOT;
38
39
    /** @var \eZ\Publish\API\Repository\Repository */
40
    private $repository;
41
42
    /** @var \Psr\Log\LoggerInterface */
43
    private $logger;
44
45
    /**
46
     * @param \eZ\Publish\API\Repository\Repository $repository
47
     * @param \Psr\Log\LoggerInterface $logger
48
     */
49
    public function __construct(Repository $repository, LoggerInterface $logger = null)
50
    {
51
        parent::__construct();
52
53
        $this->repository = $repository;
54
        $this->logger = null !== $logger ? $logger : new NullLogger();
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    protected function configure()
61
    {
62
        $beforeRunningHints = self::BEFORE_RUNNING_HINTS;
63
        $this
64
            ->setName('ezplatform:urls:regenerate-aliases')
65
            ->setDescription(
66
                'Regenerates Location URL aliases (autogenerated) and cleans up custom Location ' .
67
                'and global URL aliases stored in the Legacy Storage Engine'
68
            )
69
            ->addOption(
70
                'iteration-count',
71
                'c',
72
                InputOption::VALUE_OPTIONAL,
73
                'Number of Locations fetched into memory and processed at once',
74
                self::DEFAULT_ITERATION_COUNT
75
            )->addOption(
76
                'location-id',
77
                null,
78
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
79
                'Only Locations with provided ID\'s will have URL aliases regenerated',
80
                []
81
            )->setHelp(
82
                <<<EOT
83
{$beforeRunningHints}
84
85
The command <info>%command.name%</info> regenerates URL aliases for Locations and cleans up
86
corrupted URL aliases (pointing to non-existent Locations).
87
Existing aliases are archived (will redirect to the new ones).
88
89
Note: This script can potentially run for a very long time.
90
91
Due to performance issues the command does not send any Events.
92
93
<comment>You need to clear HTTP cache manually after executing this command.</comment>
94
95
EOT
96
            );
97
    }
98
99
    /**
100
     * Regenerate URL aliases.
101
     *
102
     * {@inheritdoc}
103
     */
104
    protected function execute(InputInterface $input, OutputInterface $output): int
105
    {
106
        $iterationCount = (int)$input->getOption('iteration-count');
107
        $locationIds = $input->getOption('location-id');
108
109
        if (!empty($locationIds)) {
110
            $locationIds = $this->getFilteredLocationList($locationIds);
111
            $locationsCount = count($locationIds);
112
        } else {
113
            $locationsCount = $this->repository->sudo(
114
                function (Repository $repository) {
115
                    return $repository->getLocationService()->getAllLocationsCount();
116
                }
117
            );
118
        }
119
120
        if ($locationsCount === 0) {
121
            $output->writeln('<info>No location was found. Exiting.</info>');
122
123
            return 0;
124
        }
125
126
        $helper = $this->getHelper('question');
127
        $question = new ConfirmationQuestion(
128
            sprintf(
129
                "<info>Found %d Locations.</info>\n%s\n<info>Do you want to proceed? [y/N] </info>",
130
                $locationsCount,
131
                self::BEFORE_RUNNING_HINTS
132
            ),
133
            false
134
        );
135
        if (!$helper->ask($input, $output, $question)) {
136
            return 0;
137
        }
138
139
        $this->regenerateSystemUrlAliases($output, $locationsCount, $locationIds, $iterationCount);
140
141
        $output->writeln('<info>Cleaning up corrupted URL aliases...</info>');
142
        $corruptedAliasesCount = $this->repository->sudo(
143
            function (Repository $repository) {
144
                return $repository->getURLAliasService()->deleteCorruptedUrlAliases();
145
            }
146
        );
147
        $output->writeln("<info>Done. Deleted {$corruptedAliasesCount} entries.</info>");
148
        $output->writeln('<comment>Make sure to clear HTTP cache.</comment>');
149
150
        return 0;
151
    }
152
153
    /**
154
     * Return configured progress bar helper.
155
     *
156
     * @param int $maxSteps
157
     * @param \Symfony\Component\Console\Output\OutputInterface $output
158
     *
159
     * @return \Symfony\Component\Console\Helper\ProgressBar
160
     */
161
    protected function getProgressBar($maxSteps, OutputInterface $output)
162
    {
163
        $progressBar = new ProgressBar($output, $maxSteps);
164
        $progressBar->setFormat(
165
            ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'
166
        );
167
168
        return $progressBar;
169
    }
170
171
    /**
172
     * Process single results page of fetched Locations.
173
     *
174
     * @param \eZ\Publish\API\Repository\Values\Content\Location[] $locations
175
     * @param \Symfony\Component\Console\Helper\ProgressBar $progressBar
176
     */
177
    private function processLocations(array $locations, ProgressBar $progressBar)
178
    {
179
        $contentList = $this->repository->sudo(
180
            function (Repository $repository) use ($locations) {
181
                $contentInfoList = array_map(
182
                    function (Location $location) {
183
                        return $location->contentInfo;
184
                    },
185
                    $locations
186
                );
187
188
                // load Content list in all languages
189
                return $repository->getContentService()->loadContentListByContentInfo(
190
                    $contentInfoList,
191
                    Language::ALL,
192
                    false
193
                );
194
            }
195
        );
196
        foreach ($locations as $location) {
197
            try {
198
                // ignore missing Content items
199
                if (!isset($contentList[$location->contentId])) {
200
                    continue;
201
                }
202
203
                $this->repository->sudo(
204
                    function (Repository $repository) use ($location) {
205
                        $repository->getURLAliasService()->refreshSystemUrlAliasesForLocation(
206
                            $location
207
                        );
208
                    }
209
                );
210
            } catch (Exception $e) {
211
                $contentInfo = $location->getContentInfo();
212
                $msg = sprintf(
213
                    'Failed processing location %d - [%d] %s (%s: %s)',
214
                    $location->id,
215
                    $contentInfo->id,
216
                    $contentInfo->name,
217
                    get_class($e),
218
                    $e->getMessage()
219
                );
220
                $this->logger->warning($msg);
221
                // in debug mode log full exception with a trace
222
                $this->logger->debug($e);
223
            } finally {
224
                $progressBar->advance(1);
225
            }
226
        }
227
    }
228
229
    /**
230
     * @param int $offset
231
     * @param int $iterationCount
232
     *
233
     * @return \eZ\Publish\API\Repository\Values\Content\Location[]
234
     *
235
     * @throws \Exception
236
     */
237
    private function loadAllLocations(int $offset, int $iterationCount): array
238
    {
239
        return $this->repository->sudo(
240
            function (Repository $repository) use ($offset, $iterationCount) {
241
                return $repository->getLocationService()->loadAllLocations($offset, $iterationCount);
242
            }
243
        );
244
    }
245
246
    /**
247
     * @param int[] $locationIds
248
     * @param int $offset
249
     * @param int $iterationCount
250
     *
251
     * @return \eZ\Publish\API\Repository\Values\Content\Location[]
252
     *
253
     * @throws \Exception
254
     */
255
    private function loadSpecificLocations(array $locationIds, int $offset, int $iterationCount): array
256
    {
257
        $locationIds = array_slice($locationIds, $offset, $iterationCount);
258
259
        return $this->repository->sudo(
260
            function (Repository $repository) use ($locationIds) {
261
                return $repository->getLocationService()->loadLocationList($locationIds);
262
            }
263
        );
264
    }
265
266
    /**
267
     * @param int[] $locationIds
268
     *
269
     * @return int[]
270
     *
271
     * @throws \Exception
272
     */
273
    private function getFilteredLocationList(array $locationIds): array
274
    {
275
        $locations = $this->repository->sudo(
276
            function (Repository $repository) use ($locationIds) {
277
                $locationService = $repository->getLocationService();
278
279
                return $locationService->loadLocationList($locationIds);
280
            }
281
        );
282
283
        return array_map(
284
            function (Location $location) {
285
                return $location->id;
286
            },
287
            $locations
288
        );
289
    }
290
291
    /**
292
     * @param \Symfony\Component\Console\Output\OutputInterface $output
293
     * @param int $locationsCount
294
     * @param int[] $locationIds
295
     * @param int $iterationCount
296
     */
297
    private function regenerateSystemUrlAliases(
298
        OutputInterface $output,
299
        int $locationsCount,
300
        array $locationIds,
301
        int $iterationCount
302
    ): void {
303
        $output->writeln('Regenerating System URL aliases...');
304
305
        $progressBar = $this->getProgressBar($locationsCount, $output);
306
        $progressBar->start();
307
308
        for ($offset = 0; $offset <= $locationsCount; $offset += $iterationCount) {
309
            gc_disable();
310
            if (!empty($locationIds)) {
311
                $locations = $this->loadSpecificLocations($locationIds, $offset, $iterationCount);
312
            } else {
313
                $locations = $this->loadAllLocations($offset, $iterationCount);
314
            }
315
            $this->processLocations($locations, $progressBar);
316
            gc_enable();
317
        }
318
        $progressBar->finish();
319
        $output->writeln('');
320
        $output->writeln('<info>Done.</info>');
321
    }
322
}
323