Completed
Push — 6.13 ( 40aa84...e4ce24 )
by
unknown
30:35 queued 10:29
created

CleanupVersionsCommand::execute()   F

Complexity

Conditions 18
Paths 352

Size

Total Lines 117

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
nc 352
nop 2
dl 0
loc 117
rs 1.7066
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Doctrine\DBAL\Connection;
10
use Exception;
11
use eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider;
12
use eZ\Publish\API\Repository\Repository;
13
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
14
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
15
use PDO;
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
22
class CleanupVersionsCommand extends Command
23
{
24
    const DEFAULT_REPOSITORY_USER = 'admin';
25
    const DEFAULT_EXCLUDED_CONTENT_TYPES = 'user';
26
27
    const BEFORE_RUNNING_HINTS = <<<EOT
28
<error>Before you continue:</error>
29
- Make sure to back up your database.
30
- Take installation offline, during the script execution the database should not be modified.
31
- Run this command without memory limit.
32
- Run this command in production environment using <info>--env=prod</info>
33
EOT;
34
35
    const VERSION_DRAFT = 'draft';
36
    const VERSION_ARCHIVED = 'archived';
37
    const VERSION_PUBLISHED = 'published';
38
    const VERSION_ALL = 'all';
39
40
    const VERSION_STATUS = [
41
        self::VERSION_DRAFT => VersionInfo::STATUS_DRAFT,
42
        self::VERSION_ARCHIVED => VersionInfo::STATUS_ARCHIVED,
43
        self::VERSION_PUBLISHED => VersionInfo::STATUS_PUBLISHED,
44
    ];
45
46
    /**
47
     * @var \eZ\Publish\API\Repository\Repository
48
     */
49
    private $repository;
50
51
    /**
52
     * @var \eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider
53
     */
54
    private $repositoryConfigurationProvider;
55
56
    /**
57
     * @var \Doctrine\DBAL\Driver\Connection
58
     */
59
    private $connection;
60
61
    public function __construct(
62
        Repository $repository,
63
        RepositoryConfigurationProvider $repositoryConfigurationProvider,
64
        Connection $connection
65
    ) {
66
        $this->repository = $repository;
67
        $this->repositoryConfigurationProvider = $repositoryConfigurationProvider;
68
        $this->connection = $connection;
69
70
        parent::__construct();
71
    }
72
73
    protected function configure()
74
    {
75
        $beforeRunningHints = self::BEFORE_RUNNING_HINTS;
76
        $this
77
            ->setName('ezplatform:content:cleanup-versions')
78
            ->setDescription('Remove unwanted content versions. It keeps published version untouched. By default, it keeps also the last archived/draft version.')
79
            ->addOption(
80
                'status',
81
                't',
82
                InputOption::VALUE_OPTIONAL,
83
                sprintf(
84
                    "Select which version types should be removed: '%s', '%s', '%s'.",
85
                    self::VERSION_DRAFT,
86
                    self::VERSION_ARCHIVED,
87
                    self::VERSION_ALL
88
                ),
89
                self::VERSION_ALL
90
            )
91
            ->addOption(
92
                'keep',
93
                'k',
94
                InputOption::VALUE_OPTIONAL,
95
                "Sets number of the most recent versions (both drafts and archived) which won't be removed.",
96
                'config_default'
97
            )
98
            ->addOption(
99
                'user',
100
                'u',
101
                InputOption::VALUE_OPTIONAL,
102
                'eZ Platform username (with Role containing at least Content policies: remove, read, versionread)',
103
                self::DEFAULT_REPOSITORY_USER
104
            )
105
            ->addOption(
106
                'excluded-content-types',
107
                null,
108
                InputOption::VALUE_OPTIONAL,
109
                'Comma separated list of ContentType identifiers of which versions should not be removed, for instance `article`.',
110
                self::DEFAULT_EXCLUDED_CONTENT_TYPES
111
            )->setHelp(
112
                <<<EOT
113
The command <info>%command.name%</info> reduces content versions to a minimum. 
114
It keeps published version untouched, and by default it keeps also the last archived/draft version.
115
Note: This script can potentially run for a very long time, and in Symfony dev environment it will consume memory exponentially with size of dataset.
116
117
{$beforeRunningHints}
118
EOT
119
            );
120
    }
121
122
    protected function execute(InputInterface $input, OutputInterface $output)
123
    {
124
        // We don't load repo services or config resolver before execute() to avoid loading before SiteAccess is set.
125
        $keep = $input->getOption('keep');
126
        if ($keep === 'config_default') {
127
            $config = $this->repositoryConfigurationProvider->getRepositoryConfig();
128
            $keep = $config['options']['default_version_archive_limit'];
129
        }
130
131
        if (($keep = (int) $keep) < 0) {
132
            throw new InvalidArgumentException(
133
                'keep',
134
                'Keep value can not be negative.'
135
            );
136
        }
137
138
        $userService = $this->repository->getUserService();
139
        $contentService = $this->repository->getContentService();
140
        $permissionResolver = $this->repository->getPermissionResolver();
141
142
        $permissionResolver->setCurrentUserReference(
143
            $userService->loadUserByLogin($input->getOption('user'))
144
        );
145
146
        $status = $input->getOption('status');
147
148
        $excludedContentTypeIdentifiers = explode(',', $input->getOption('excluded-content-types'));
149
        $contentIds = $this->getObjectsIds($keep, $status, $excludedContentTypeIdentifiers);
150
        $contentIdsCount = count($contentIds);
151
152
        if ($contentIdsCount === 0) {
153
            $output->writeln('<info>There is no Content matching given criteria.</info>');
154
155
            return;
156
        }
157
158
        $output->writeln(sprintf(
159
            '<info>Found %d Content IDs matching given criteria.</info>',
160
            $contentIdsCount
161
        ));
162
163
        $displayProgressBar = !($output->isVerbose() || $output->isVeryVerbose() || $output->isDebug());
164
165
        if ($displayProgressBar) {
166
            $progressBar = new ProgressBar($output, $contentIdsCount);
167
            $progressBar->setFormat(
168
                '%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%' . PHP_EOL
169
            );
170
            $progressBar->start();
171
        }
172
173
        $removedVersionsCounter = 0;
174
175
        $removeAll = $status === self::VERSION_ALL;
176
        $removeDrafts = $status === self::VERSION_DRAFT;
177
        $removeArchived = $status === self::VERSION_ARCHIVED;
178
179
        foreach ($contentIds as $contentId) {
180
            try {
181
                $contentInfo = $contentService->loadContentInfo((int) $contentId);
182
                $versions = $contentService->loadVersions($contentInfo);
183
                $versionsCount = count($versions);
184
185
                $output->writeln(sprintf(
186
                    '<info>Content %d has %d version(s)</info>',
187
                    (int) $contentId,
188
                    $versionsCount
189
                ), OutputInterface::VERBOSITY_VERBOSE);
190
191
                $versions = array_filter($versions, function ($version) use ($removeAll, $removeDrafts, $removeArchived) {
192
                    if (
193
                        ($removeAll && $version->status !== VersionInfo::STATUS_PUBLISHED) ||
194
                        ($removeDrafts && $version->status === VersionInfo::STATUS_DRAFT) ||
195
                        ($removeArchived && $version->status === VersionInfo::STATUS_ARCHIVED)
196
                    ) {
197
                        return true;
198
                    }
199
                });
200
201
                if ($keep > 0) {
202
                    $versions = array_slice($versions, 0, -$keep);
203
                }
204
205
                $output->writeln(sprintf(
206
                    "Found %d content's (%d) version(s) to remove.",
207
                    count($versions),
208
                    (int) $contentId
209
                ), OutputInterface::VERBOSITY_VERBOSE);
210
211
                /** @var \eZ\Publish\API\Repository\Values\Content\VersionInfo $version */
212
                foreach ($versions as $version) {
213
                    $contentService->deleteVersion($version);
214
                    ++$removedVersionsCounter;
215
                    $output->writeln(sprintf(
216
                        "Content's (%d) version (%d) has been deleted.",
217
                        $contentInfo->id,
218
                        $version->id
219
                    ), OutputInterface::VERBOSITY_VERBOSE);
220
                }
221
222
                if ($displayProgressBar) {
223
                    $progressBar->advance(1);
0 ignored issues
show
Bug introduced by
The variable $progressBar does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
224
                }
225
            } catch (Exception $e) {
226
                $output->writeln(sprintf(
227
                    '<error>%s</error>',
228
                    $e->getMessage()
229
                ));
230
            }
231
        }
232
233
        $output->writeln(sprintf(
234
            '<info>Removed %d unwanted contents version(s) from %d content(s).</info>',
235
            $removedVersionsCounter,
236
            $contentIdsCount
237
        ));
238
    }
239
240
    /**
241
     * @param int $keep
242
     * @param string $status
243
     * @param string[] $excludedContentTypes
244
     *
245
     * @return array
246
     *
247
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
248
     */
249
    protected function getObjectsIds($keep, $status, $excludedContentTypes = [])
250
    {
251
        $query = $this->connection->createQueryBuilder()
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\DBAL\Driver\Connection as the method createQueryBuilder() does only exist in the following implementations of said interface: Doctrine\DBAL\Connection, Doctrine\DBAL\Connections\MasterSlaveConnection, Doctrine\DBAL\Portability\Connection, Doctrine\DBAL\Sharding\PoolingShardConnection.

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...
252
                ->select('c.id')
253
                ->from('ezcontentobject', 'c')
254
                ->join('c', 'ezcontentobject_version', 'v', 'v.contentobject_id = c.id')
255
                ->join('c', 'ezcontentclass', 'cl', 'cl.id = c.contentclass_id')
256
                ->groupBy('c.id', 'v.status')
257
                ->having('count(c.id) > :keep');
258
        $query->setParameter('keep', $keep);
259
260
        if ($status !== self::VERSION_ALL) {
261
            $query->where('v.status = :status');
262
            $query->setParameter('status', $this->mapStatusToVersionInfoStatus($status));
263
        } else {
264
            $query->andWhere('v.status != :status');
265
            $query->setParameter('status', $this->mapStatusToVersionInfoStatus(self::VERSION_PUBLISHED));
266
        }
267
268
        if ($excludedContentTypes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $excludedContentTypes of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
269
            $expr = $query->expr();
270
            $query
271
                ->andWhere(
272
                    $expr->notIn(
273
                        'cl.identifier',
274
                        ':contentTypes'
275
                    )
276
                )->setParameter(':contentTypes', $excludedContentTypes, Connection::PARAM_STR_ARRAY);
277
        }
278
279
        $stmt = $query->execute();
280
281
        return $stmt->fetchAll(PDO::FETCH_COLUMN);
282
    }
283
284
    /**
285
     * @param string $status
286
     *
287
     * @return int
288
     *
289
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
290
     */
291
    private function mapStatusToVersionInfoStatus($status)
292
    {
293
        if (array_key_exists($status, self::VERSION_STATUS)) {
294
            return self::VERSION_STATUS[$status];
295
        }
296
297
        throw new InvalidArgumentException(
298
            'status',
299
            sprintf(
300
                "Status %s can't be mapped to VersionInfo status.",
301
                $status
302
            )
303
        );
304
    }
305
}
306