Passed
Push — master ( c8b9d6...4facea )
by Nicolaas
02:05
created

RunForOneObject::getTableSizes()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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