RunForOneObject::findBestSuitedTemplates()   A
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 6
eloc 10
c 4
b 0
f 0
nc 7
nop 1
dl 0
loc 23
rs 9.2222
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\OnlyLastOnes;
18
use Sunnysideup\VersionPruner\PruningTemplates\SiteTreeVersioningTemplate;
19
20
class RunForOneObject
21
{
22
    use Configurable;
23
    use Injectable;
24
25
    /**
26
     * Versioned DataObject.
27
     *
28
     * @var DataObject
29
     */
30
    protected $object;
31
32
    /**
33
     * array of Version numbers to delete.
34
     *
35
     * @var array
36
     */
37
    protected $toDelete = [];
38
39
    /**
40
     * reversed list of templates (most specific first).
41
     *
42
     * @var array
43
     */
44
    protected $templatesAvailable = [];
45
46
    /**
47
     * list of tables to delete per class name.
48
     *
49
     * @var array
50
     */
51
    protected $tablesPerClassName = [];
52
53
    /**
54
     * list of templates per class name.
55
     *
56
     * @var array
57
     */
58
    protected $templatesPerClassName = [];
59
60
    /**
61
     * @var bool
62
     */
63
    protected $verbose = false;
64
65
    /**
66
     * @var bool
67
     */
68
    protected $dryRun = false;
69
70
    /**
71
     * @var array
72
     */
73
    protected $countPerTableRegister = [];
74
75
    /**
76
     * schema is:
77
     * ```php
78
     *     ClassName => [
79
     *         PruningTemplateClassName1 => [
80
     *             "PropertyName1" => Value1
81
     *             "PropertyName2" => Value2
82
     *         ],
83
     *         PruningTemplateClassName2 => [
84
     *         ],
85
     *     ]
86
     * ```.
87
     * N.B. least specific first!
88
     *
89
     * @var array
90
     */
91
    private static $templates = [
92
        'default' => [
93
            BasedOnTimeScale::class => [],
94
        ],
95
        SiteTree::class => [
96
            Drafts::class => [],
97
            SiteTreeVersioningTemplate::class => [],
98
        ],
99
        File::class => [
100
            DeleteFiles::class => [],
101
            // OnlyLastOnes::class => [],
102
        ],
103
    ];
104
105
    public function __construct()
106
    {
107
        $this->gatherTemplates();
108
    }
109
110
    public static function inst()
111
    {
112
        return Injector::inst()->get(static::class);
113
    }
114
115
    public function setVerbose(?bool $verbose = true): self
116
    {
117
        $this->verbose = $verbose;
118
119
        return $this;
120
    }
121
122
    public function setDryRun(?bool $dryRun = true): self
123
    {
124
        $this->dryRun = $dryRun;
125
126
        return $this;
127
    }
128
129
    /**
130
     * returns the total number deleted.
131
     *
132
     * @param DataObject $object
133
     */
134
    public function getTableSizes($object, ?bool $lastOnly = false): array
135
    {
136
        $this->object = $object;
137
        $array = [];
138
        if ($this->isValidObject()) {
139
            $queriedTables = $this->getTablesForClassName();
140
            // print_r($this->toDelete[$this->getUniqueKey()]);
141
            foreach ($queriedTables as $table) {
142
                $array[$table] = $this->getCountPerTable($table);
143
            }
144
        }
145
        if (count($array) && $lastOnly) {
146
            $lastKey = array_key_last($array);
147
148
            return [
149
                $lastKey => $array[$lastKey],
150
            ];
151
        }
152
153
        return $array;
154
    }
155
156
    public function getRootTable(string $className): ?string
157
    {
158
        if (class_exists($className)) {
159
            $queriedTables = $this->getTablesForClassName($className);
160
            // print_r($this->toDelete[$this->getUniqueKey()]);
161
            foreach ($queriedTables as $table) {
162
                return $table;
163
            }
164
        }
165
166
        return null;
167
    }
168
169
    /**
170
     * returns the total number deleted.
171
     *
172
     * @param DataObject $object
173
     *
174
     * @return int number of deletions
175
     */
176
    public function deleteSuperfluousVersions($object): int
177
    {
178
        $this->object = $object;
179
        if (! $this->isValidObject()) {
180
            echo $object->ClassName . ' ERROR';
181
182
            return 0;
183
        }
184
        // reset to reduce size ...
185
        $this->toDelete = [];
186
187
        $this->workoutWhatNeedsDeleting();
188
189
        // Base table has Versioned data
190
        $totalDeleted = 0;
191
192
        // Ugly (borrowed from DataObject::class), but returns all
193
        // database tables relating to DataObject
194
        $queriedTables = $this->getTablesForClassName();
195
        // print_r($this->toDelete[$this->getUniqueKey()]);
196
        foreach ($queriedTables as $table) {
197
            $overallCount = $this->getCountPerTable($table);
198
            if ($this->verbose) {
199
                $selectToBeDeletedSQL = '
200
                    SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
201
                    WHERE "RecordID" = ' . (int) $this->object->ID;
202
                $totalRows = DB::query($selectToBeDeletedSQL)->value();
203
                DB::alteration_message('... ... ... Number of rows for current object in ' . $table . ': ' . $totalRows);
204
            }
205
            if (count($this->toDelete[$this->getUniqueKey()]) > 0) {
206
                if (true === $this->dryRun) {
207
                    $selectToBeDeletedSQL = '
208
                        SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
209
                        WHERE
210
                            "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
211
                            AND "RecordID" = ' . (int) $this->object->ID;
212
213
                    $toBeDeletedCount = DB::query($selectToBeDeletedSQL)->value();
214
                    $totalDeleted += $toBeDeletedCount;
215
                    if ($this->verbose) {
216
                        DB::alteration_message('... ... ... running ' . $selectToBeDeletedSQL);
217
                        DB::alteration_message('... ... ... total rows to be deleted  ... ' . $toBeDeletedCount . ' of ' . $overallCount);
218
                    }
219
                } else {
220
                    $delSQL = '
221
                        DELETE FROM "' . $table . '_Versions"
222
                        WHERE
223
                            "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
224
                            AND "RecordID" = ' . (int) $this->object->ID;
225
226
                    DB::query($delSQL);
227
                    $count = DB::affected_rows();
228
                    $totalDeleted += $count;
229
                    $overallCount -= $count;
230
                    if ($this->verbose) {
231
                        DB::alteration_message('... ... ... running ' . $delSQL);
232
                        DB::alteration_message('... ... ... total rows deleted ... ' . $totalDeleted);
233
                    }
234
                }
235
            }
236
            $this->addCountRegister($table, $overallCount);
237
        }
238
239
        return $totalDeleted;
240
    }
241
242
    /**
243
     * returns the total number deleted.
244
     *
245
     * @param DataObject $object
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 gatherTemplates()
274
    {
275
        $this->templatesAvailable = array_reverse(
276
            $this->Config()->get('templates'),
277
            true //important - to preserve keys!
278
        );
279
        // remove skips
280
        foreach ($this->templatesAvailable as $className => $runnerClassNameWithOptions) {
281
            if ('skip' === $runnerClassNameWithOptions) {
282
                $this->templatesAvailable[$className] = 'skip';
283
284
                continue;
285
            }
286
            if (is_array($runnerClassNameWithOptions)) {
287
                foreach ($runnerClassNameWithOptions as $runnerClassName => $options) {
288
                    if ('skip' === $options) {
289
                        unset($this->templatesAvailable[$className][$runnerClassName]);
290
291
                        continue;
292
                    }
293
                }
294
            }
295
        }
296
    }
297
298
    protected function workoutWhatNeedsDeleting()
299
    {
300
        // array of version IDs to delete
301
        // IMPORTANT
302
        if (! isset($this->toDelete[$this->getUniqueKey()])) {
303
            $this->toDelete[$this->getUniqueKey()] = [];
304
        }
305
306
        $myTemplates = $this->findBestSuitedTemplates(false);
307
        if (is_array($myTemplates) && $myTemplates !== []) {
308
            foreach ($myTemplates as $className => $options) {
309
                $runner = new $className($this->object, $this->toDelete[$this->getUniqueKey()]);
310
                if ($this->verbose) {
311
                    DB::alteration_message('... ... ... Running ' . $runner->getTitle() . ': ' . $runner->getDescription());
312
                }
313
314
                foreach ($options as $key => $value) {
315
                    $method = 'set' . $key;
316
                    $runner->{$method}($value);
317
                }
318
319
                $runner->run();
320
                // print_r($runner->getToDelete());
321
                $this->toDelete[$this->getUniqueKey()] += $runner->getToDelete();
322
323
                if ($this->verbose) {
324
                    DB::alteration_message('... ... ... Total versions to delete now ' . count($this->toDelete[$this->getUniqueKey()]));
325
                }
326
            }
327
        }
328
    }
329
330
    /**
331
     * we use this to make sure we never mix up two records.
332
     */
333
    protected function getUniqueKey(): string
334
    {
335
        return $this->object->ClassName . '_' . $this->object->ID;
336
    }
337
338
    protected function hasStages(): bool
339
    {
340
        $hasStages = false;
341
        if ($this->object->hasMethod('hasStages')) {
342
            $oldMode = Versioned::get_reading_mode();
343
            if ('Stage.Stage' !== $oldMode) {
344
                Versioned::set_reading_mode('Stage.Stage');
345
            }
346
347
            $hasStages = (bool) $this->object->hasStages();
348
            if ('Stage.Stage' !== $oldMode) {
349
                Versioned::set_reading_mode($oldMode);
350
            }
351
        }
352
353
        return $hasStages;
354
    }
355
356
    protected function findBestSuitedTemplates(?bool $forExplanation = false)
357
    {
358
        $templates = $this->Config()->get('templates');
359
        $classesWithOptions = [];
360
        if (empty($this->templatesPerClassName[$this->object->ClassName]) || $forExplanation) {
361
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
362
                if ($this->object instanceof $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;
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
        // do not use exists here - as it has a different meaning for files and folders.
403
        return $this->object && $this->object->ID;
404
    }
405
406
    protected function getTablesForClassName(?string $className = ''): array
407
    {
408
        if (! $className) {
409
            $className = $this->object->ClassName;
410
        }
411
        if (empty($this->tablesPerClassName[$className])) {
412
            // $classTables = []
413
            // $allClasses = ClassInfo::subclassesFor($this->object->ClassName, true);
414
            // foreach ($allClasses as $class) {
415
            //     if (DataObject::getSchema()->classHasTable($class)) {
416
            //         $classTables[] = DataObject::getSchema()->tableName($class);
417
            //     }
418
            // }
419
            // $this->tablesPerClassName[$this->object->ClassName] = array_unique($classTables);
420
            $id = $this->object->ID ?? 0;
421
            $srcQuery = DataList::create($className)
422
                ->filter('ID', intval($id))
423
                ->dataQuery()
424
                ->query()
425
            ;
426
            $this->tablesPerClassName[$className] = $srcQuery->queriedTables();
427
        }
428
429
        return $this->tablesPerClassName[$className];
430
    }
431
432
    protected function addCountRegister(string $tableName, int $count): void
433
    {
434
        $this->countPerTableRegister[$tableName] = $count;
435
    }
436
437
    protected function getCountPerTable(string $table): int
438
    {
439
        $overallCount = $this->countPerTableRegister[$table] ?? -1;
440
        if (-1 === $overallCount) {
441
            $selectOverallCountSQL = '
442
                SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"';
443
            $overallCount = DB::query($selectOverallCountSQL)->value();
444
        }
445
446
        return $overallCount;
447
    }
448
}
449