Passed
Push — master ( 4facea...42ecbc )
by Nicolaas
02:04
created

RunForOneObject::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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
                $this->templatesAvailable[$className] = 'skip';
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
     */
157
    public function getTableSizes($object, ?bool $lastOnly = true): array
158
    {
159
        $this->object = $object;
160
        $array = [];
161
        if ($this->isValidObject()) {
162
            $queriedTables = $this->getTablesForClassName();
163
            // print_r($this->toDelete[$this->getUniqueKey()]);
164
            foreach ($queriedTables as $table) {
165
                $array[$table] = $this->getCountPerTable($table);
166
            }
167
        }
168
        if(count($array) && $lastOnly) {
169
            $lastKey = array_key_last($array);
170
            return [
171
                $lastKey => $array[$lastKey],
172
            ];
173
        }
174
        return $array;
175
    }
176
177
    /**
178
     * returns the total number deleted.
179
     *
180
     * @param DataObject $object
181
     *
182
     */
183
    public function getRootTable($object): ?string
184
    {
185
        $this->object = $object;
186
        $array = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $array is dead and can be removed.
Loading history...
187
        if ($this->isValidObject()) {
188
            $queriedTables = $this->getTablesForClassName();
189
            // print_r($this->toDelete[$this->getUniqueKey()]);
190
            foreach ($queriedTables as $table) {
191
                return $table;
192
            }
193
        }
194
        return null;
195
    }
196
197
    /**
198
     * returns the total number deleted.
199
     *
200
     * @param DataObject $object
201
     *
202
     * @return int number of deletions
203
     */
204
    public function deleteSuperfluousVersions($object): int
205
    {
206
        $this->object = $object;
207
        if (! $this->isValidObject()) {
208
            return 0;
209
        }
210
        // reset to reduce size ...
211
        $this->toDelete = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type string of property $toDelete.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
212
213
        $this->workoutWhatNeedsDeleting();
214
215
        // Base table has Versioned data
216
        $totalDeleted = 0;
217
218
        // Ugly (borrowed from DataObject::class), but returns all
219
        // database tables relating to DataObject
220
        $queriedTables = $this->getTablesForClassName();
221
        // print_r($this->toDelete[$this->getUniqueKey()]);
222
        foreach ($queriedTables as $table) {
223
            $overallCount = $this->getCountPerTable($table);
224
            if($this->verbose) {
225
                $selectToBeDeletedSQL = '
226
                    SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
227
                    WHERE "RecordID" = ' . (int) $this->object->ID;
228
                $totalRows = DB::query($selectToBeDeletedSQL)->value();
229
                DB::alteration_message('... ... ... Number of rows for current object in '.$table.': '.$totalRows);
230
            }
231
            if (count($this->toDelete[$this->getUniqueKey()])) {
232
                if (true === $this->dryRun) {
233
                    $selectToBeDeletedSQL = '
234
                        SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
235
                        WHERE
236
                            "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
237
                            AND "RecordID" = ' . (int) $this->object->ID;
238
239
                    $toBeDeletedCount = DB::query($selectToBeDeletedSQL)->value();
240
                    $totalDeleted += $toBeDeletedCount;
241
                    if ($this->verbose) {
242
                        DB::alteration_message('... ... ... running ' . $selectToBeDeletedSQL);
243
                        DB::alteration_message('... ... ... total rows to be deleted  ... ' . $toBeDeletedCount . ' of ' . $overallCount);
244
                    }
245
                } else {
246
                    $delSQL = '
247
                        DELETE FROM "' . $table . '_Versions"
248
                        WHERE
249
                            "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
250
                            AND "RecordID" = ' . (int) $this->object->ID;
251
252
                    DB::query($delSQL);
253
                    $count = DB::affected_rows();
254
                    $totalDeleted += $count;
255
                    $overallCount -= $count;
256
                    if ($this->verbose) {
257
                        DB::alteration_message('... ... ... running ' . $delSQL);
258
                        DB::alteration_message('... ... ... total rows deleted ... ' . $totalDeleted);
259
                    }
260
                }
261
            }
262
            $this->addCountRegister($table, $overallCount);
263
        }
264
265
        return $totalDeleted;
266
    }
267
268
    /**
269
     * returns the total number deleted.
270
     *
271
     * @param DataObject $object
272
     * @param bool       $verbose
273
     */
274
    public function getTemplatesDescription($object): array
275
    {
276
        $array = [];
277
        $this->object = $object;
278
        if ($this->isValidObject()) {
279
            $myTemplates = $this->findBestSuitedTemplates(true);
280
            if(is_array($myTemplates) && count($myTemplates)) {
281
                foreach ($myTemplates as $className => $options) {
282
                    if(class_exists($className)) {
283
                        $runner = new $className($this->object, []);
284
                        $array[] = $runner->getTitle() . ': ' . $runner->getDescription();
285
                    } else {
286
                        $array[] = $options;
287
                    }
288
                }
289
            }
290
        }
291
292
        return $array;
293
    }
294
295
    public function getCountRegister(): array
296
    {
297
        return $this->countPerTableRegister;
298
    }
299
300
    protected function workoutWhatNeedsDeleting()
301
    {
302
        // array of version IDs to delete
303
        // IMPORTANT
304
        if(! isset($this->toDelete[$this->getUniqueKey()])) {
305
            $this->toDelete[$this->getUniqueKey()] = [];
306
        }
307
308
        $myTemplates = $this->findBestSuitedTemplates(false);
309
        if(is_array($myTemplates) && !empty($myTemplates)) {
310
            foreach ($myTemplates as $className => $options) {
311
                $runner = new $className($this->object, $this->toDelete[$this->getUniqueKey()]);
312
                if ($this->verbose) {
313
                    DB::alteration_message('... ... ... Running ' . $runner->getTitle() . ': ' . $runner->getDescription());
314
                }
315
316
                foreach ($options as $key => $value) {
317
                    $method = 'set' . $key;
318
                    $runner->{$method}($value);
319
                }
320
321
                $runner->run();
322
                // print_r($runner->getToDelete());
323
                $this->toDelete[$this->getUniqueKey()] += $runner->getToDelete();
324
325
                if ($this->verbose) {
326
                    DB::alteration_message('... ... ... Total versions to delete now ' . count($this->toDelete[$this->getUniqueKey()]));
327
                }
328
            }
329
        }
330
    }
331
332
    /**
333
     * we use this to make sure we never mix up two records.
334
     */
335
    protected function getUniqueKey(): string
336
    {
337
        return $this->object->ClassName . '_' . $this->object->ID;
338
    }
339
340
    protected function hasStages(): bool
341
    {
342
        $hasStages = false;
343
        if ($this->object->hasMethod('hasStages')) {
344
            $oldMode = Versioned::get_reading_mode();
345
            if ('Stage.Stage' !== $oldMode) {
346
                Versioned::set_reading_mode('Stage.Stage');
347
            }
348
349
            $hasStages = (bool) $this->object->hasStages();
350
            if ('Stage.Stage' !== $oldMode) {
351
                Versioned::set_reading_mode($oldMode);
352
            }
353
        }
354
355
        return $hasStages;
356
    }
357
358
    protected function findBestSuitedTemplates(?bool $forExplanation = false)
359
    {
360
        if (empty($this->templatesPerClassName[$this->object->ClassName]) || $forExplanation) {
361
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
362
                if (is_a($this->object, $className)) {
363
                    // if($forExplanation && $className !== $this->object->ClassName) {
364
                    //     echo "$className !== {$this->object->ClassName}";
365
                    //     $this->templatesPerClassName[$this->object->ClassName] = ['As '.$className];
366
                    // }
367
                    $this->templatesPerClassName[$this->object->ClassName] = $classesWithOptions;
368
369
                    break;
370
                }
371
            }
372
373
            if (! isset($this->templatesPerClassName[$this->object->ClassName])) {
374
                $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 361. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
375
            }
376
        }
377
378
        return $this->templatesPerClassName[$this->object->ClassName];
379
    }
380
381
    protected function isValidObject(): bool
382
    {
383
        if (false === $this->hasStages()) {
384
            if ($this->verbose) {
385
                DB::alteration_message('... ... ... Error, no stages', 'deleted');
386
            }
387
388
            return false;
389
        }
390
391
        // if (! $this->object->hasMethod('isLiveVersion')) {
392
        //     return false;
393
        // }
394
        //
395
        // if (false === $this->object->isLiveVersion()) {
396
        //     if ($this->verbose) {
397
        //         DB::alteration_message('... ... ... Error, not a live version', 'deleted');
398
        //     }
399
        //
400
        //     return false;
401
        // }
402
403
        return $this->object && $this->object->exists();
404
    }
405
406
    protected function getTablesForClassName(): array
407
    {
408
        if (empty($this->tablesPerClassName[$this->object->ClassName])) {
409
            // $classTables = []
410
            // $allClasses = ClassInfo::subclassesFor($this->object->ClassName, true);
411
            // foreach ($allClasses as $class) {
412
            //     if (DataObject::getSchema()->classHasTable($class)) {
413
            //         $classTables[] = DataObject::getSchema()->tableName($class);
414
            //     }
415
            // }
416
            // $this->tablesPerClassName[$this->object->ClassName] = array_unique($classTables);
417
418
            $srcQuery = DataList::create($this->object->ClassName)
419
                ->filter('ID', $this->object->ID)
420
                ->dataQuery()
421
                ->query()
422
            ;
423
            $this->tablesPerClassName[$this->object->ClassName] = $srcQuery->queriedTables();
424
        }
425
426
        return $this->tablesPerClassName[$this->object->ClassName];
427
    }
428
429
    protected function addCountRegister(string $tableName, int $count): void
430
    {
431
        $this->countPerTableRegister[$tableName] = $count;
432
    }
433
434
435
    protected function getCountPerTable(string $table) : int
436
    {
437
        $overallCount = $this->countPerTableRegister[$table] ?? -1;
438
        if($overallCount === -1) {
439
            $selectOverallCountSQL = '
440
                SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"';
441
            $overallCount = DB::query($selectOverallCountSQL)->value();
442
        }
443
444
        return $overallCount;
445
    }
446
447
}
448