Passed
Push — master ( abeb78...9e896a )
by Nicolaas
02:13
created

RunForOneObject   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 142
c 10
b 0
f 0
dl 0
loc 378
rs 6.96
wmc 53

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A addCountRegister() 0 3 1
A findBestSuitedTemplates() 0 21 6
A isValidObject() 0 23 4
A getCountRegister() 0 3 1
A gatherTemplates() 0 17 6
A hasStages() 0 16 4
A getTemplatesDescription() 0 19 6
A getUniqueKey() 0 3 1
A getCountPerTable() 0 10 2
A getTablesForClassName() 0 21 2
B deleteSuperfluousVersions() 0 60 8
A setVerbose() 0 5 1
A inst() 0 3 1
B workoutWhatNeedsDeleting() 0 27 8
A setDryRun() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like RunForOneObject often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RunForOneObject, and based on these observations, apply Extract Interface, too.

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->gatherTemplates();
107
    }
108
109
    protected function gatherTemplates()
110
    {
111
        $this->templatesAvailable = array_reverse(
112
            $this->Config()->get('templates'),
113
            true //important - to preserve keys!
114
        );
115
        // remove skips
116
        foreach($this->templatesAvailable as $className => $runnerClassNameWithOptions) {
117
            if($runnerClassNameWithOptions === 'skip') {
118
                unset($this->templatesAvailable[$className]);
119
                continue;
120
            }
121
            if(is_array($runnerClassNameWithOptions)) {
122
                foreach($runnerClassNameWithOptions as $runnerClassName => $options) {
123
                    if($options === 'skip') {
124
                        unset($this->templatesAvailable[$className][$runnerClassName]);
125
                        continue;
126
                    }
127
                }
128
            }
129
        }
130
    }
131
132
    public static function inst()
133
    {
134
        return Injector::inst()->get(static::class);
135
    }
136
137
    public function setVerbose(?bool $verbose = true): self
138
    {
139
        $this->verbose = $verbose;
140
141
        return $this;
142
    }
143
144
    public function setDryRun(?bool $dryRun = true): self
145
    {
146
        $this->dryRun = $dryRun;
147
148
        return $this;
149
    }
150
151
    /**
152
     * returns the total number deleted.
153
     *
154
     * @param DataObject $object
155
     *
156
     * @return int number of deletions
157
     */
158
    public function deleteSuperfluousVersions($object): int
159
    {
160
        $this->object = $object;
161
        if (! $this->isValidObject()) {
162
            return 0;
163
        }
164
165
        $this->workoutWhatNeedsDeleting();
166
167
        // Base table has Versioned data
168
        $totalDeleted = 0;
169
170
        // Ugly (borrowed from DataObject::class), but returns all
171
        // database tables relating to DataObject
172
        $queriedTables = $this->getTablesForClassName();
173
        // print_r($this->toDelete[$this->getUniqueKey()]);
174
        foreach ($queriedTables as $table) {
175
            $overallCount = $this->getCountPerTable($table);
176
            if($this->verbose) {
177
                $selectToBeDeletedSQL = '
178
                    SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
179
                    WHERE "RecordID" = ' . (int) $this->object->ID;
180
                $totalRows = DB::query($selectToBeDeletedSQL)->value();
181
                DB::alteration_message('... ... ... Number of rows for current object in '.$table.': '.$totalRows);
182
            }
183
            if (count($this->toDelete[$this->getUniqueKey()])) {
184
                if (true === $this->dryRun) {
185
                    $selectToBeDeletedSQL = '
186
                        SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
187
                        WHERE
188
                            "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
189
                            AND "RecordID" = ' . (int) $this->object->ID;
190
191
                    $toBeDeletedCount = DB::query($selectToBeDeletedSQL)->value();
192
                    $totalDeleted += $toBeDeletedCount;
193
                    if ($this->verbose) {
194
                        DB::alteration_message('... ... ... running ' . $selectToBeDeletedSQL);
195
                        DB::alteration_message('... ... ... total rows to be deleted  ... ' . $toBeDeletedCount . ' of ' . $overallCount);
196
                    }
197
                } else {
198
                    $delSQL = '
199
                        DELETE FROM "' . $table . '_Versions"
200
                        WHERE
201
                            "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
202
                            AND "RecordID" = ' . (int) $this->object->ID;
203
204
                    DB::query($delSQL);
205
                    $count = DB::affected_rows();
206
                    $totalDeleted += $count;
207
                    $overallCount -= $count;
208
                    if ($this->verbose) {
209
                        DB::alteration_message('... ... ... running ' . $delSQL);
210
                        DB::alteration_message('... ... ... total rows deleted ... ' . $totalDeleted);
211
                    }
212
                }
213
            }
214
            $this->addCountRegister($table, $overallCount);
215
        }
216
217
        return $totalDeleted;
218
    }
219
220
    /**
221
     * returns the total number deleted.
222
     *
223
     * @param DataObject $object
224
     * @param bool       $verbose
225
     */
226
    public function getTemplatesDescription($object): array
227
    {
228
        $array = [];
229
        $this->object = $object;
230
        if ($this->isValidObject()) {
231
            $myTemplates = $this->findBestSuitedTemplates(true);
232
            if(is_array($myTemplates) && count($myTemplates)) {
233
                foreach ($myTemplates as $className => $options) {
234
                    if(class_exists($className)) {
235
                        $runner = new $className($this->object, []);
236
                        $array[] = $runner->getTitle() . ': ' . $runner->getDescription();
237
                    } else {
238
                        $array[] = $options;
239
                    }
240
                }
241
            }
242
        }
243
244
        return $array;
245
    }
246
247
    public function getCountRegister(): array
248
    {
249
        return $this->countPerTableRegister;
250
    }
251
252
    protected function workoutWhatNeedsDeleting()
253
    {
254
        // array of version IDs to delete
255
        // IMPORTANT
256
        if(! isset($this->toDelete[$this->getUniqueKey()])) {
257
            $this->toDelete[$this->getUniqueKey()] = [];
258
        }
259
260
        $myTemplates = $this->findBestSuitedTemplates(false);
261
        if(is_array($myTemplates) && !empty($myTemplates)) {
262
            foreach ($myTemplates as $className => $options) {
263
                $runner = new $className($this->object, $this->toDelete[$this->getUniqueKey()]);
264
                if ($this->verbose) {
265
                    DB::alteration_message('... ... ... Running ' . $runner->getTitle() . ': ' . $runner->getDescription());
266
                }
267
268
                foreach ($options as $key => $value) {
269
                    $method = 'set' . $key;
270
                    $runner->{$method}($value);
271
                }
272
273
                $runner->run();
274
                // print_r($runner->getToDelete());
275
                $this->toDelete[$this->getUniqueKey()] += $runner->getToDelete();
276
277
                if ($this->verbose) {
278
                    DB::alteration_message('... ... ... Total versions to delete now ' . count($this->toDelete[$this->getUniqueKey()]));
279
                }
280
            }
281
        }
282
    }
283
284
    /**
285
     * we use this to make sure we never mix up two records.
286
     */
287
    protected function getUniqueKey(): string
288
    {
289
        return $this->object->ClassName . '_' . $this->object->ID;
290
    }
291
292
    protected function hasStages(): bool
293
    {
294
        $hasStages = false;
295
        if ($this->object->hasMethod('hasStages')) {
296
            $oldMode = Versioned::get_reading_mode();
297
            if ('Stage.Stage' !== $oldMode) {
298
                Versioned::set_reading_mode('Stage.Stage');
299
            }
300
301
            $hasStages = (bool) $this->object->hasStages();
302
            if ('Stage.Stage' !== $oldMode) {
303
                Versioned::set_reading_mode($oldMode);
304
            }
305
        }
306
307
        return $hasStages;
308
    }
309
310
    protected function findBestSuitedTemplates(?bool $forExplanation = false)
311
    {
312
        if (empty($this->templatesPerClassName[$this->object->ClassName]) || $forExplanation) {
313
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
314
                if (is_a($this->object, $className)) {
315
                    // if($forExplanation && $className !== $this->object->ClassName) {
316
                    //     echo "$className !== {$this->object->ClassName}";
317
                    //     $this->templatesPerClassName[$this->object->ClassName] = ['As '.$className];
318
                    // }
319
                    $this->templatesPerClassName[$this->object->ClassName] = $classesWithOptions;
320
321
                    break;
322
                }
323
            }
324
325
            if (! isset($this->templatesPerClassName[$this->object->ClassName])) {
326
                $this->templatesPerClassName[$this->object->ClassName] = $templates['default'] ?? $classesWithOptions;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $classesWithOptions seems to be defined by a foreach iteration on line 313. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
Comprehensibility Best Practice introduced by
The variable $templates seems to never exist and therefore isset should always be false.
Loading history...
327
            }
328
        }
329
330
        return $this->templatesPerClassName[$this->object->ClassName];
331
    }
332
333
    protected function isValidObject(): bool
334
    {
335
        if (false === $this->hasStages()) {
336
            if ($this->verbose) {
337
                DB::alteration_message('... ... ... Error, no stages', 'deleted');
338
            }
339
340
            return false;
341
        }
342
343
        // if (! $this->object->hasMethod('isLiveVersion')) {
344
        //     return false;
345
        // }
346
        //
347
        // if (false === $this->object->isLiveVersion()) {
348
        //     if ($this->verbose) {
349
        //         DB::alteration_message('... ... ... Error, not a live version', 'deleted');
350
        //     }
351
        //
352
        //     return false;
353
        // }
354
355
        return $this->object && $this->object->exists();
356
    }
357
358
    protected function getTablesForClassName(): array
359
    {
360
        if (empty($this->tablesPerClassName[$this->object->ClassName])) {
361
            // $classTables = []
362
            // $allClasses = ClassInfo::subclassesFor($this->object->ClassName, true);
363
            // foreach ($allClasses as $class) {
364
            //     if (DataObject::getSchema()->classHasTable($class)) {
365
            //         $classTables[] = DataObject::getSchema()->tableName($class);
366
            //     }
367
            // }
368
            // $this->tablesPerClassName[$this->object->ClassName] = array_unique($classTables);
369
370
            $srcQuery = DataList::create($this->object->ClassName)
371
                ->filter('ID', $this->object->ID)
372
                ->dataQuery()
373
                ->query()
374
            ;
375
            $this->tablesPerClassName[$this->object->ClassName] = $srcQuery->queriedTables();
376
        }
377
378
        return $this->tablesPerClassName[$this->object->ClassName];
379
    }
380
381
    protected function addCountRegister(string $tableName, int $count): void
382
    {
383
        $this->countPerTableRegister[$tableName] = $count;
384
    }
385
386
387
    protected function getCountPerTable(string $table) : int
388
    {
389
        $overallCount = $this->countPerTableRegister[$table] ?? -1;
390
        if($overallCount === -1) {
391
            $selectOverallCountSQL = '
392
                SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"';
393
            $overallCount = DB::query($selectOverallCountSQL)->value();
394
        }
395
396
        return $overallCount;
397
    }
398
399
}
400