Completed
Push — 7.5 ( baa0ac...d5fa5f )
by
unknown
20:06
created

RegenerateUrlAliasesCommand::loadAllLocations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
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 installation offline, during the script execution the database should not be modified.
34
- Run this command without memory limit, i.e. processing of 300k Locations 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 Signals.
92
93
<comment>HTTP cache needs to be cleared 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)
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;
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;
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 afterwards.</comment>');
149
    }
150
151
    /**
152
     * Return configured progress bar helper.
153
     *
154
     * @param int $maxSteps
155
     * @param \Symfony\Component\Console\Output\OutputInterface $output
156
     *
157
     * @return \Symfony\Component\Console\Helper\ProgressBar
158
     */
159
    protected function getProgressBar($maxSteps, OutputInterface $output)
160
    {
161
        $progressBar = new ProgressBar($output, $maxSteps);
162
        $progressBar->setFormat(
163
            ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'
164
        );
165
166
        return $progressBar;
167
    }
168
169
    /**
170
     * Process single results page of fetched Locations.
171
     *
172
     * @param \eZ\Publish\API\Repository\Values\Content\Location[] $locations
173
     * @param \Symfony\Component\Console\Helper\ProgressBar $progressBar
174
     */
175
    private function processLocations(array $locations, ProgressBar $progressBar)
176
    {
177
        $contentList = $this->repository->sudo(
178
            function (Repository $repository) use ($locations) {
179
                $contentInfoList = array_map(
180
                    function (Location $location) {
181
                        return $location->contentInfo;
182
                    },
183
                    $locations
184
                );
185
186
                // load Content list in all languages
187
                return $repository->getContentService()->loadContentListByContentInfo(
188
                    $contentInfoList,
189
                    Language::ALL,
190
                    false
191
                );
192
            }
193
        );
194
        foreach ($locations as $location) {
195
            try {
196
                // ignore missing Content items
197
                if (!isset($contentList[$location->contentId])) {
198
                    continue;
199
                }
200
201
                $this->repository->sudo(
202
                    function (Repository $repository) use ($location) {
203
                        $repository->getURLAliasService()->refreshSystemUrlAliasesForLocation(
204
                            $location
205
                        );
206
                    }
207
                );
208
            } catch (Exception $e) {
209
                $contentInfo = $location->getContentInfo();
210
                $msg = sprintf(
211
                    'Failed processing location %d - [%d] %s (%s: %s)',
212
                    $location->id,
213
                    $contentInfo->id,
214
                    $contentInfo->name,
215
                    get_class($e),
216
                    $e->getMessage()
217
                );
218
                $this->logger->warning($msg);
219
                // in debug mode log full exception with a trace
220
                $this->logger->debug($e);
221
            } finally {
222
                $progressBar->advance(1);
223
            }
224
        }
225
    }
226
227
    /**
228
     * @param int $offset
229
     * @param int $iterationCount
230
     *
231
     * @return \eZ\Publish\API\Repository\Values\Content\Location[]
232
     *
233
     * @throws \Exception
234
     */
235
    private function loadAllLocations(int $offset, int $iterationCount): array
236
    {
237
        return $this->repository->sudo(
238
            function (Repository $repository) use ($offset, $iterationCount) {
239
                return $repository->getLocationService()->loadAllLocations($offset, $iterationCount);
240
            }
241
        );
242
    }
243
244
    /**
245
     * @param int[] $locationIds
246
     * @param int $offset
247
     * @param int $iterationCount
248
     *
249
     * @return \eZ\Publish\API\Repository\Values\Content\Location[]
250
     *
251
     * @throws \Exception
252
     */
253
    private function loadSpecificLocations(array $locationIds, int $offset, int $iterationCount): array
254
    {
255
        $locationIds = array_slice($locationIds, $offset, $iterationCount);
256
257
        return $this->repository->sudo(
258
            function (Repository $repository) use ($locationIds) {
259
                return $repository->getLocationService()->loadLocationList($locationIds);
260
            }
261
        );
262
    }
263
264
    /**
265
     * @param int[] $locationIds
266
     *
267
     * @return int[]
268
     *
269
     * @throws \Exception
270
     */
271
    private function getFilteredLocationList(array $locationIds): array
272
    {
273
        $locations = $this->repository->sudo(
274
            function (Repository $repository) use ($locationIds) {
275
                $locationService = $repository->getLocationService();
276
277
                return $locationService->loadLocationList($locationIds);
278
            }
279
        );
280
281
        return array_map(
282
            function (Location $location) {
283
                return $location->id;
284
            },
285
            $locations
286
        );
287
    }
288
289
    /**
290
     * @param \Symfony\Component\Console\Output\OutputInterface $output
291
     * @param int $locationsCount
292
     * @param int[] $locationIds
293
     * @param int $iterationCount
294
     */
295
    private function regenerateSystemUrlAliases(
296
        OutputInterface $output,
297
        int $locationsCount,
298
        array $locationIds,
299
        int $iterationCount
300
    ): void {
301
        $output->writeln('Regenerating System URL aliases...');
302
303
        $progressBar = $this->getProgressBar($locationsCount, $output);
304
        $progressBar->start();
305
306
        for ($offset = 0; $offset <= $locationsCount; $offset += $iterationCount) {
307
            gc_disable();
308
            if (!empty($locationIds)) {
309
                $locations = $this->loadSpecificLocations($locationIds, $offset, $iterationCount);
310
            } else {
311
                $locations = $this->loadAllLocations($offset, $iterationCount);
312
            }
313
            $this->processLocations($locations, $progressBar);
314
            gc_enable();
315
        }
316
        $progressBar->finish();
317
        $output->writeln('');
318
        $output->writeln('<info>Done.</info>');
319
    }
320
}
321