Completed
Push — master ( 3e0b85...9d0d49 )
by André
63:27 queued 41:30
created

CleanupVersionsCommand::execute()   C

Complexity

Conditions 13
Paths 21

Size

Total Lines 86

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
nc 21
nop 2
dl 0
loc 86
rs 5.5987
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\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\Output;
20
use Symfony\Component\Console\Output\OutputInterface;
21
22
class CleanupVersionsCommand extends Command
23
{
24
    const DEFAULT_REPOSITORY_USER = 'admin';
25
26
    const VERSION_DRAFT = 'draft';
27
    const VERSION_ARCHIVED = 'archived';
28
    const VERSION_PUBLISHED = 'published';
29
    const VERSION_ALL = 'all';
30
31
    const VERSION_STATUS = [
32
        self::VERSION_DRAFT => VersionInfo::STATUS_DRAFT,
33
        self::VERSION_ARCHIVED => VersionInfo::STATUS_ARCHIVED,
34
        self::VERSION_PUBLISHED => VersionInfo::STATUS_PUBLISHED,
35
    ];
36
37
    /**
38
     * @var \eZ\Publish\API\Repository\Repository
39
     */
40
    private $repository;
41
42
    /**
43
     * @var \eZ\Publish\API\Repository\UserService
44
     */
45
    private $userService;
46
47
    /**
48
     * @var \eZ\Publish\API\Repository\ContentService
49
     */
50
    private $contentService;
51
52
    /**
53
     * @var \eZ\Publish\API\Repository\PermissionResolver
54
     */
55
    private $permissionResolver;
56
57
    /**
58
     * @var \eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider
59
     */
60
    private $repositoryConfigurationProvider;
61
62
    /**
63
     * @var \Doctrine\DBAL\Driver\Connection
64
     */
65
    private $connection;
66
67
    public function __construct(
68
        Repository $repository,
69
        RepositoryConfigurationProvider $repositoryConfigurationProvider,
70
        Connection $connection
71
    ) {
72
        $this->repository = $repository;
73
        $this->repositoryConfigurationProvider = $repositoryConfigurationProvider;
74
        $this->connection = $connection;
75
76
        parent::__construct();
77
    }
78
79 View Code Duplication
    protected function initialize(InputInterface $input, OutputInterface $output)
80
    {
81
        parent::initialize($input, $output);
82
83
        $this->userService = $this->repository->getUserService();
84
        $this->contentService = $this->repository->getContentService();
85
        $this->permissionResolver = $this->repository->getPermissionResolver();
86
87
        $this->permissionResolver->setCurrentUserReference(
88
            $this->userService->loadUserByLogin($input->getOption('user'))
89
        );
90
    }
91
92
    protected function configure()
93
    {
94
        $config = $this->repositoryConfigurationProvider->getRepositoryConfig();
95
96
        $this
97
            ->setName('ezplatform:content:cleanup-versions')
98
            ->setDescription('Remove unwanted content versions. It keeps published version untouched. By default, it keeps also the last archived/draft version.')
99
            ->addOption(
100
                'status',
101
                't',
102
                InputOption::VALUE_OPTIONAL,
103
                sprintf(
104
                    "Select which version types should be removed: '%s', '%s', '%s'.",
105
                    self::VERSION_DRAFT,
106
                    self::VERSION_ARCHIVED,
107
                    self::VERSION_ALL
108
                ),
109
                self::VERSION_ALL
110
            )
111
            ->addOption(
112
                'keep',
113
                'k',
114
                InputOption::VALUE_OPTIONAL,
115
                "Sets number of the most recent versions (both drafts and archived) which won't be removed.",
116
                $config['options']['default_version_archive_limit']
117
            )
118
            ->addOption(
119
                'user',
120
                'u',
121
                InputOption::VALUE_OPTIONAL,
122
                'eZ Platform username (with Role containing at least Content policies: remove, read, versionread)',
123
                self::DEFAULT_REPOSITORY_USER
124
            );
125
    }
126
127
    protected function execute(InputInterface $input, OutputInterface $output)
128
    {
129
        if (($keep = (int) $input->getOption('keep')) < 0) {
130
            throw new InvalidArgumentException(
131
                'status',
132
                'Keep value can not be negative.'
133
            );
134
        }
135
136
        $status = $input->getOption('status');
137
138
        $contentIds = $this->getObjectsIds($keep, $status);
139
        $contentIdsCount = count($contentIds);
140
141
        if ($contentIdsCount === 0) {
142
            $output->writeln('<info>There is no Content matching given criteria.</info>');
143
144
            return;
145
        }
146
147
        $output->writeln(sprintf(
148
            '<info>Found %d Content IDs matching given criteria.</info>',
149
            $contentIdsCount
150
        ));
151
152
        $removedVersionsCounter = 0;
153
154
        $removeAll = $status === self::VERSION_ALL;
155
        $removeDrafts = $status === self::VERSION_DRAFT;
156
        $removeArchived = $status === self::VERSION_ARCHIVED;
157
158
        foreach ($contentIds as $contentId) {
159
            try {
160
                $contentInfo = $this->contentService->loadContentInfo((int) $contentId);
161
                $versions = $this->contentService->loadVersions($contentInfo);
162
                $versionsCount = count($versions);
163
164
                $output->writeln(sprintf(
165
                    '<info>Content %d has %d version(s)</info>',
166
                    (int) $contentId,
167
                    $versionsCount
168
                ), Output::VERBOSITY_VERBOSE);
169
170
                $versions = array_filter($versions, function ($version) use ($removeAll, $removeDrafts, $removeArchived) {
171
                    if (
172
                        ($removeAll && $version->status !== VersionInfo::STATUS_PUBLISHED) ||
173
                        ($removeDrafts && $version->status === VersionInfo::STATUS_DRAFT) ||
174
                        ($removeArchived && $version->status === VersionInfo::STATUS_ARCHIVED)
175
                    ) {
176
                        return $version;
177
                    }
178
                });
179
180
                if ($keep > 0) {
181
                    $versions = array_slice($versions, 0, -$keep);
182
                }
183
184
                $output->writeln(sprintf(
185
                    "Found %d content's (%d) version(s) to remove.",
186
                    count($versions),
187
                    (int) $contentId
188
                ), Output::VERBOSITY_VERBOSE);
189
190
                /** @var \eZ\Publish\API\Repository\Values\Content\VersionInfo $version */
191
                foreach ($versions as $version) {
192
                    $this->contentService->deleteVersion($version);
193
                    ++$removedVersionsCounter;
194
                    $output->writeln(sprintf(
195
                        "Content's (%d) version (%d) has been deleted.",
196
                        $contentInfo->id,
197
                        $version->id
198
                    ), Output::VERBOSITY_VERBOSE);
199
                }
200
            } catch (Exception $e) {
201
                $output->writeln(sprintf(
202
                    '<error>%s</error>',
203
                    $e->getMessage()
204
                ));
205
            }
206
        }
207
208
        $output->writeln(sprintf(
209
            '<info>Removed %d unwanted contents version(s).</info>',
210
            $removedVersionsCounter
211
        ));
212
    }
213
214
    /**
215
     * @param int $keep
216
     * @param string $status
217
     *
218
     * @return array
219
     *
220
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
221
     */
222
    protected function getObjectsIds($keep, $status)
223
    {
224
        $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...
225
                ->select('c.id')
226
                ->from('ezcontentobject', 'c')
227
                ->join('c', 'ezcontentobject_version', 'v', 'v.contentobject_id = c.id')
228
                ->groupBy('c.id', 'v.status')
229
                ->having('count(c.id) > :keep');
230
        $query->setParameter('keep', $keep);
231
232
        if ($status !== self::VERSION_ALL) {
233
            $query->where('v.status = :status');
234
            $query->setParameter('status', $this->mapStatusToVersionInfoStatus($status));
235
        } else {
236
            $query->andWhere('v.status != :status');
237
            $query->setParameter('status', $this->mapStatusToVersionInfoStatus(self::VERSION_PUBLISHED));
238
        }
239
240
        $stmt = $query->execute();
241
242
        return $stmt->fetchAll(PDO::FETCH_COLUMN);
243
    }
244
245
    /**
246
     * @param string $status
247
     *
248
     * @return int
249
     *
250
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
251
     */
252
    private function mapStatusToVersionInfoStatus($status)
253
    {
254
        if (array_key_exists($status, self::VERSION_STATUS)) {
255
            return self::VERSION_STATUS[$status];
256
        }
257
258
        throw new InvalidArgumentException(
259
            'status',
260
            sprintf(
261
                "Status %s can't be mapped to VersionInfo status.",
262
                $status
263
            )
264
        );
265
    }
266
}
267