Completed
Push — master ( 70d1cb...b61b4a )
by André
83:39 queued 66:02
created

CleanupVersionsCommand::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 12
Ratio 100 %

Importance

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