Completed
Push — 6.7 ( eacd1f...2a3684 )
by
unknown
25:16
created

RegenerateUrlAliasesCommand::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 31
rs 9.424
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 eZ\Publish\Core\MVC\Symfony\Cache\GatewayCachePurger;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\NullLogger;
16
use Symfony\Component\Console\Command\Command;
17
use Symfony\Component\Console\Helper\ProgressBar;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Console\Input\InputOption;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Console\Question\ConfirmationQuestion;
22
23
/**
24
 * The ezplatform:urls:regenerate-aliases Symfony command implementation.
25
 * Recreates system URL aliases for all existing Locations and cleanups corrupted URL alias nodes.
26
 */
27
class RegenerateUrlAliasesCommand extends Command
28
{
29
    const DEFAULT_ITERATION_COUNT = 1000;
30
31
    const BEFORE_RUNNING_HINTS = <<<EOT
32
<error>Before you continue:</error>
33
- Make sure to back up your database.
34
- Take installation offline, during the script execution the database should not be modified.
35
- Run this command without memory limit, i.e. processing of 300k Locations can take up to 1 GB of RAM.
36
- Run this command in production environment using <info>--env=prod</info>
37
EOT;
38
39
    /**
40
     * @var \eZ\Publish\API\Repository\Repository
41
     */
42
    private $repository;
43
44
    /**
45
     * @var \Psr\Log\LoggerInterface
46
     */
47
    private $logger;
48
49
    /**
50
     * @var \eZ\Publish\Core\MVC\Symfony\Cache\GatewayCachePurger
51
     */
52
    private $cachePurger;
53
54
    /**
55
     * @param \eZ\Publish\API\Repository\Repository $repository
56
     * @param \eZ\Publish\Core\MVC\Symfony\Cache\GatewayCachePurger $cachePurger
57
     * @param \Psr\Log\LoggerInterface $logger
58
     */
59
    public function __construct(
60
        Repository $repository,
61
        GatewayCachePurger $cachePurger,
62
        LoggerInterface $logger = null
63
    ) {
64
        parent::__construct();
65
66
        $this->repository = $repository;
67
        $this->cachePurger = $cachePurger;
68
        $this->logger = null !== $logger ? $logger : new NullLogger();
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74
    protected function configure()
75
    {
76
        $beforeRunningHints = self::BEFORE_RUNNING_HINTS;
77
        $this
78
            ->setName('ezplatform:urls:regenerate-aliases')
79
            ->setDescription(
80
                'Regenerates Location URL aliases (autogenerated) and cleans up custom Location ' .
81
                'and global URL aliases stored in the Legacy Storage Engine'
82
            )
83
            ->addOption(
84
                'iteration-count',
85
                'c',
86
                InputOption::VALUE_OPTIONAL,
87
                'Number of Locations fetched into memory and processed at once',
88
                self::DEFAULT_ITERATION_COUNT
89
            )
90
            ->setHelp(
91
                <<<EOT
92
{$beforeRunningHints}
93
94
The command <info>%command.name%</info> regenerates URL aliases for Locations and cleans up
95
corrupted URL aliases (pointing to non-existent Locations).
96
Existing aliases are archived (will redirect to the new ones).
97
98
Note: This script can potentially run for a very long time.
99
100
Due to performance issues the command does not send any Signals.
101
102
EOT
103
            );
104
    }
105
106
    /**
107
     * Regenerate URL aliases.
108
     *
109
     * {@inheritdoc}
110
     */
111
    protected function execute(InputInterface $input, OutputInterface $output)
112
    {
113
        $iterationCount = (int)$input->getOption('iteration-count');
114
115
        $locationsCount = $this->repository->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
116
            function (Repository $repository) {
117
                return $repository->getLocationService()->getAllLocationsCount();
118
            }
119
        );
120
121
        $helper = $this->getHelper('question');
122
        $question = new ConfirmationQuestion(
123
            sprintf(
124
                "<info>Found %d Locations.</info>\n%s\n<info>Do you want to proceed? [y/N] </info>",
125
                $locationsCount,
126
                self::BEFORE_RUNNING_HINTS
127
            ),
128
            false
129
        );
130
        if (!$helper->ask($input, $output, $question)) {
131
            return;
132
        }
133
134
        $output->writeln('<info>Cleaning up corrupted URL aliases...</info>');
135
        $corruptedAliasesCount = $this->repository->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
136
            function (Repository $repository) {
137
                return $repository->getURLAliasService()->deleteCorruptedUrlAliases();
138
            }
139
        );
140
        $output->writeln("<info>Done. Deleted {$corruptedAliasesCount} entries.</info>");
141
142
        $output->writeln('Regenerating System URL aliases...');
143
144
        $progressBar = $this->getProgressBar($locationsCount, $output);
145
        $progressBar->start();
146
147
        for ($offset = 0; $offset <= $locationsCount; $offset += $iterationCount) {
148
            gc_disable();
149
            $locations = $this->repository->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
150
                function (Repository $repository) use ($offset, $iterationCount) {
151
                    return $repository->getLocationService()->loadAllLocations($offset, $iterationCount);
152
                }
153
            );
154
            $this->processLocations($locations, $progressBar);
155
            gc_enable();
156
        }
157
        $progressBar->finish();
158
        $output->writeln('');
159
160
        $output->writeln('Purging HTTP cache...');
161
        $this->cachePurger->purgeAll();
162
        $output->writeln('<info>Done.</info>');
163
    }
164
165
    /**
166
     * Return configured progress bar helper.
167
     *
168
     * @param int $maxSteps
169
     * @param \Symfony\Component\Console\Output\OutputInterface $output
170
     *
171
     * @return \Symfony\Component\Console\Helper\ProgressBar
172
     */
173
    protected function getProgressBar($maxSteps, OutputInterface $output)
174
    {
175
        $progressBar = new ProgressBar($output, $maxSteps);
176
        $progressBar->setFormat(
177
            ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'
178
        );
179
180
        return $progressBar;
181
    }
182
183
    /**
184
     * Process single results page of fetched Locations.
185
     *
186
     * @param \eZ\Publish\API\Repository\Values\Content\Location[] $locations
187
     * @param \Symfony\Component\Console\Helper\ProgressBar $progressBar
188
     */
189
    private function processLocations(array $locations, ProgressBar $progressBar)
190
    {
191
        $contentList = $this->repository->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
192
            function (Repository $repository) use ($locations) {
193
                $contentInfoList = array_map(
194
                    function (Location $location) {
195
                        return $location->contentInfo;
196
                    },
197
                    $locations
198
                );
199
200
                // load Content list in all languages
201
                return $repository->getContentService()->loadContentListByContentInfo(
202
                    $contentInfoList,
203
                    Language::ALL,
204
                    false
205
                );
206
            }
207
        );
208
        foreach ($locations as $location) {
209
            try {
210
                // ignore missing Content items
211
                if (!isset($contentList[$location->contentId])) {
212
                    continue;
213
                }
214
215
                $content = $contentList[$location->contentId];
216
                $this->repository->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
217
                    function (Repository $repository) use ($location, $content) {
218
                        $repository->getURLAliasService()->refreshSystemUrlAliasesForLocation(
219
                            $location,
220
                            $content
221
                        );
222
                    }
223
                );
224
            } catch (Exception $e) {
225
                $contentInfo = $location->getContentInfo();
226
                $msg = sprintf(
227
                    'Failed processing location %d - [%d] %s (%s: %s)',
228
                    $location->id,
229
                    $contentInfo->id,
230
                    $contentInfo->name,
231
                    get_class($e),
232
                    $e->getMessage()
233
                );
234
                $this->logger->warning($msg);
235
                // in debug mode log full exception with a trace
236
                $this->logger->debug($e);
237
            } finally {
238
                $progressBar->advance(1);
239
            }
240
        }
241
    }
242
}
243