Passed
Push — master ( 8f132f...6d656d )
by Nicolaas
02:02
created

RunForOneObject::gatherTemplates()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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