Completed
Push — EZP-30546 ( a67f89 )
by André
33:24 queued 09:46
created

CleanupVersionsCommand::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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