Passed
Push — master ( 29ed1e...268413 )
by Nicolaas
02:09
created

RunForOneObject::workoutWhatNeedsDeleting()   B

Complexity

Conditions 8
Paths 20

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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