Passed
Push — master ( 002b8d...c92847 )
by Nicolaas
02:38
created

RunForOneObject::deleteSuperfluousVersions()   C

Complexity

Conditions 12
Paths 91

Size

Total Lines 82
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 12
eloc 50
c 6
b 0
f 0
nc 91
nop 1
dl 0
loc 82
rs 6.9666

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
namespace Sunnysideup\VersionPruner\Api;
4
5
use SilverStripe\Assets\File;
6
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTree was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\Core\Injector\Injectable;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DB;
13
use SilverStripe\Versioned\Versioned;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Versioned\Versioned was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Sunnysideup\VersionPruner\PruningTemplates\BasedOnTimeScale;
15
use Sunnysideup\VersionPruner\PruningTemplates\DeleteFiles;
16
use Sunnysideup\VersionPruner\PruningTemplates\Drafts;
17
use Sunnysideup\VersionPruner\PruningTemplates\SiteTreeVersioningTemplate;
18
19
class RunForOneObject
20
{
21
    use Configurable;
22
    use Injectable;
23
24
    /**
25
     * Versioned DataObject.
26
     *
27
     * @var DataObject
28
     */
29
    protected $object;
30
31
    /**
32
     * array of Version numbers to delete.
33
     *
34
     * @var string
35
     */
36
    protected $toDelete = [];
37
38
    /**
39
     * reversed list of templates (most specific first).
40
     *
41
     * @var array
42
     */
43
    protected $templatesAvailable = [];
44
45
    /**
46
     * list of tables to delete per class name.
47
     *
48
     * @var array
49
     */
50
    protected $tablesPerClassName = [];
51
52
    /**
53
     * list of templates per class name.
54
     *
55
     * @var array
56
     */
57
    protected $templatesPerClassName = [];
58
59
    /**
60
     * @var bool
61
     */
62
    protected $verbose = false;
63
64
    /**
65
     * @var bool
66
     */
67
    protected $dryRun = false;
68
69
    /**
70
     * @var array
71
     */
72
    protected $countPerTableRegister = [];
73
74
    /**
75
     * schema is:
76
     * ```php
77
     *     ClassName => [
78
     *         PruningTemplateClassName1 => [
79
     *             "PropertyName1" => Value1
80
     *             "PropertyName2" => Value2
81
     *         ],
82
     *         PruningTemplateClassName2 => [
83
     *         ],
84
     *     ]
85
     * ```.
86
     * N.B. least specific first!
87
     *
88
     * @var array
89
     */
90
    private static $templates = [
91
        'default' => [
92
            BasedOnTimeScale::class => [],
93
        ],
94
        SiteTree::class => [
95
            Drafts::class => [],
96
            SiteTreeVersioningTemplate::class => [],
97
        ],
98
        File::class => [
99
            DeleteFiles::class => [],
100
            BasedOnTimeScale::class => [],
101
        ],
102
    ];
103
104
    public function __construct()
105
    {
106
        $this->templatesAvailable = array_reverse(
107
            $this->Config()->get('templates'),
108
            true //important - to preserve keys!
109
        );
110
    }
111
112
    public static function inst()
113
    {
114
        return Injector::inst()->get(static::class);
115
    }
116
117
    public function setVerbose(?bool $verbose = true): self
118
    {
119
        $this->verbose = $verbose;
120
121
        return $this;
122
    }
123
124
    public function setDryRun(?bool $dryRun = true): self
125
    {
126
        $this->dryRun = $dryRun;
127
128
        return $this;
129
    }
130
131
    /**
132
     * returns the total number deleted.
133
     *
134
     * @param DataObject $object
135
     *
136
     * @return int number of deletions
137
     */
138
    public function deleteSuperfluousVersions($object): int
139
    {
140
        $this->object = $object;
141
        if (! $this->isValidObject()) {
142
            return 0;
143
        }
144
145
        // array of version IDs to delete
146
        // IMPORTANT
147
        $this->toDelete[$this->getUniqueKey()] = [];
148
149
        // Base table has Versioned data
150
        $totalDeleted = 0;
151
152
        $myTemplates = $this->findBestSuitedTemplates();
153
        foreach ($myTemplates as $className => $options) {
154
            $runner = new $className($this->object, $this->toDelete[$this->getUniqueKey()]);
155
            if ($this->verbose) {
156
                DB::alteration_message('... ... ... Running ' . $runner->getTitle() . ': ' . $runner->getDescription());
157
            }
158
159
            foreach ($options as $key => $value) {
160
                $method = 'set' . $key;
161
                $runner->{$method}($value);
162
            }
163
164
            $runner->run();
165
            $this->toDelete[$this->getUniqueKey()] = $runner->getToDelete();
166
167
            if ($this->verbose) {
168
                DB::alteration_message('... ... ... total versions to delete now ' . count($this->toDelete[$this->getUniqueKey()]));
169
            }
170
        }
171
172
        if (! count($this->toDelete[$this->getUniqueKey()])) {
173
            return 0;
174
        }
175
176
        // Ugly (borrowed from DataObject::class), but returns all
177
        // database tables relating to DataObject
178
        $queriedTables = $this->getTablesForClassName();
179
        foreach ($queriedTables as $table) {
180
            $overallCount = $this->countPerTableRegister[$table] ?? -1;
181
            if($overallCount === -1) {
182
                $selectOverallCountSQL = '
183
                    SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"';
184
                $overallCount = DB::query($selectOverallCountSQL)->value();
185
            }
186
            if (true === $this->dryRun) {
187
                $selectToBeDeletedSQL = '
188
                    SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
189
                    WHERE
190
                        "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
191
                        AND "RecordID" = ' . (int) $this->object->ID;
192
193
                $toBeDeletedCount = DB::query($selectToBeDeletedSQL)->value();
194
                $totalDeleted += $toBeDeletedCount;
195
                if ($this->verbose) {
196
                    DB::alteration_message('... ... ... running ' . $select);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $select does not exist. Did you maybe mean $selectOverallCountSQL?
Loading history...
197
                    DB::alteration_message('... ... ... total rows to be deleted  ... ' . $toBeDeletedCount . ' of ' . $overallCount);
198
                }
199
            } else {
200
                $delSQL = '
201
                    DELETE FROM "' . $table . '_Versions"
202
                    WHERE
203
                        "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
204
                        AND "RecordID" = ' . (int) $this->object->ID;
205
206
                DB::query($delSQL);
207
                $count = DB::affected_rows();
208
                $totalDeleted += $count;
209
                $overallCount -= $count;
210
                if ($this->verbose) {
211
                    DB::alteration_message('... ... ... running ' . $delSQL);
212
                    DB::alteration_message('... ... ... total rows deleted ... ' . $totalDeleted);
213
                }
214
            }
215
216
            $this->addCountRegister($table, $overallCount);
217
        }
218
219
        return $totalDeleted;
220
    }
221
222
    /**
223
     * returns the total number deleted.
224
     *
225
     * @param DataObject $object
226
     * @param bool       $verbose
227
     */
228
    public function getTemplatesDescription($object): array
229
    {
230
        $array = [];
231
        $this->object = $object;
232
        if ($this->isValidObject()) {
233
            $myTemplates = $this->findBestSuitedTemplates();
234
            foreach ($myTemplates as $className => $options) {
235
                $runner = new $className($this->object, []);
236
                $array[] = $runner->getTitle() . ': ' . $runner->getDescription();
237
            }
238
        }
239
240
        return $array;
241
    }
242
243
    public function getCountRegister(): array
244
    {
245
        return $this->countPerTableRegister;
246
    }
247
248
    /**
249
     * we use this to make sure we never mix up two records.
250
     */
251
    protected function getUniqueKey(): string
252
    {
253
        return $this->object->ClassName . '_' . $this->object->ID;
254
    }
255
256
    protected function hasStages(): bool
257
    {
258
        $hasStages = false;
259
        if ($this->object->hasMethod('hasStages')) {
260
            $oldMode = Versioned::get_reading_mode();
261
            if ('Stage.Stage' !== $oldMode) {
262
                Versioned::set_reading_mode('Stage.Stage');
263
            }
264
265
            $hasStages = (bool) $this->object->hasStages();
266
            if ('Stage.Stage' !== $oldMode) {
267
                Versioned::set_reading_mode($oldMode);
268
            }
269
        }
270
271
        return $hasStages;
272
    }
273
274
    protected function findBestSuitedTemplates()
275
    {
276
        if (empty($this->templatesPerClassName[$this->object->ClassName])) {
277
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
278
                if (is_a($this->object, $className)) {
279
                    $this->templatesPerClassName[$this->object->ClassName] = $classesWithOptions;
280
281
                    break;
282
                }
283
            }
284
285
            if (! isset($this->templatesPerClassName[$this->object->ClassName])) {
286
                $this->templatesPerClassName[$this->object->ClassName] = $templates['default'] ?? $classesWithOptions;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $templates seems to never exist and therefore isset should always be false.
Loading history...
Comprehensibility Best Practice introduced by
The variable $classesWithOptions seems to be defined by a foreach iteration on line 277. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
287
            }
288
        }
289
290
        return $this->templatesPerClassName[$this->object->ClassName];
291
    }
292
293
    protected function isValidObject(): bool
294
    {
295
        if (false === $this->hasStages()) {
296
            if ($this->verbose) {
297
                DB::alteration_message('... ... ... Error, no stages', 'deleted');
298
            }
299
300
            return false;
301
        }
302
303
        if (! $this->object->hasMethod('isLiveVersion')) {
304
            return false;
305
        }
306
307
        if (false === $this->object->isLiveVersion()) {
308
            if ($this->verbose) {
309
                DB::alteration_message('... ... ... Error, not a live version', 'deleted');
310
            }
311
312
            return false;
313
        }
314
315
        return $this->object && $this->object->exists();
316
    }
317
318
    protected function getTablesForClassName(): array
319
    {
320
        if (empty($this->tablesPerClassName[$this->object->ClassName])) {
321
            // $classTables = []
322
            // $allClasses = ClassInfo::subclassesFor($this->object->ClassName, true);
323
            // foreach ($allClasses as $class) {
324
            //     if (DataObject::getSchema()->classHasTable($class)) {
325
            //         $classTables[] = DataObject::getSchema()->tableName($class);
326
            //     }
327
            // }
328
            // $this->tablesPerClassName[$this->object->ClassName] = array_unique($classTables);
329
330
            $srcQuery = DataList::create($this->object->ClassName)
331
                ->filter('ID', $this->object->ID)
332
                ->dataQuery()
333
                ->query()
334
            ;
335
            $this->tablesPerClassName[$this->object->ClassName] = $srcQuery->queriedTables();
336
        }
337
338
        return $this->tablesPerClassName[$this->object->ClassName];
339
    }
340
341
    protected function addCountRegister(string $tableName, int $count): void
342
    {
343
        $this->countPerTableRegister[$tableName] = $count;
344
    }
345
}
346