Passed
Push — master ( 6eec4e...0daa52 )
by Nicolaas
08:19
created

RunForOneObject::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
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
        // array of version IDs to delete
146
        // IMPORTANT
147
        $this->toDelete[$this->getUniqueKey()] = [];
148
149
        // Base table has Versioned data
150
        $totalDeleted = 0;
151
152
        $myTemplates = $this->findBestSuitedTemplates();
153
        if(is_array($myTemplates) && !empty($myTemplates)) {
154
            foreach ($myTemplates as $className => $options) {
155
                $runner = new $className($this->object, $this->toDelete[$this->getUniqueKey()]);
156
                if ($this->verbose) {
157
                    DB::alteration_message('... ... ... Running ' . $runner->getTitle() . ': ' . $runner->getDescription());
158
                }
159
160
                foreach ($options as $key => $value) {
161
                    $method = 'set' . $key;
162
                    $runner->{$method}($value);
163
                }
164
165
                $runner->run();
166
                $this->toDelete[$this->getUniqueKey()] = $runner->getToDelete();
167
168
                if ($this->verbose) {
169
                    DB::alteration_message('... ... ... total versions to delete now ' . count($this->toDelete[$this->getUniqueKey()]));
170
                }
171
            }
172
        }
173
        // Ugly (borrowed from DataObject::class), but returns all
174
        // database tables relating to DataObject
175
        $queriedTables = $this->getTablesForClassName();
176
        foreach ($queriedTables as $table) {
177
            $overallCount = $this->getCountPerTable($table);
178
            if (! count($this->toDelete[$this->getUniqueKey()])) {
179
                $this->addCountRegister($table, $overallCount);
180
                return 0;
181
            }
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
            foreach ($myTemplates as $className => $options) {
231
                $runner = new $className($this->object, []);
232
                $array[] = $runner->getTitle() . ': ' . $runner->getDescription();
233
            }
234
        }
235
236
        return $array;
237
    }
238
239
    public function getCountRegister(): array
240
    {
241
        return $this->countPerTableRegister;
242
    }
243
244
    /**
245
     * we use this to make sure we never mix up two records.
246
     */
247
    protected function getUniqueKey(): string
248
    {
249
        return $this->object->ClassName . '_' . $this->object->ID;
250
    }
251
252
    protected function hasStages(): bool
253
    {
254
        $hasStages = false;
255
        if ($this->object->hasMethod('hasStages')) {
256
            $oldMode = Versioned::get_reading_mode();
257
            if ('Stage.Stage' !== $oldMode) {
258
                Versioned::set_reading_mode('Stage.Stage');
259
            }
260
261
            $hasStages = (bool) $this->object->hasStages();
262
            if ('Stage.Stage' !== $oldMode) {
263
                Versioned::set_reading_mode($oldMode);
264
            }
265
        }
266
267
        return $hasStages;
268
    }
269
270
    protected function findBestSuitedTemplates()
271
    {
272
        if (empty($this->templatesPerClassName[$this->object->ClassName])) {
273
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
274
                if (is_a($this->object, $className)) {
275
                    $this->templatesPerClassName[$this->object->ClassName] = $classesWithOptions;
276
277
                    break;
278
                }
279
            }
280
281
            if (! isset($this->templatesPerClassName[$this->object->ClassName])) {
282
                $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 273. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
283
            }
284
        }
285
286
        return $this->templatesPerClassName[$this->object->ClassName];
287
    }
288
289
    protected function isValidObject(): bool
290
    {
291
        if (false === $this->hasStages()) {
292
            if ($this->verbose) {
293
                DB::alteration_message('... ... ... Error, no stages', 'deleted');
294
            }
295
296
            return false;
297
        }
298
299
        if (! $this->object->hasMethod('isLiveVersion')) {
300
            return false;
301
        }
302
303
        if (false === $this->object->isLiveVersion()) {
304
            if ($this->verbose) {
305
                DB::alteration_message('... ... ... Error, not a live version', 'deleted');
306
            }
307
308
            return false;
309
        }
310
311
        return $this->object && $this->object->exists();
312
    }
313
314
    protected function getTablesForClassName(): array
315
    {
316
        if (empty($this->tablesPerClassName[$this->object->ClassName])) {
317
            // $classTables = []
318
            // $allClasses = ClassInfo::subclassesFor($this->object->ClassName, true);
319
            // foreach ($allClasses as $class) {
320
            //     if (DataObject::getSchema()->classHasTable($class)) {
321
            //         $classTables[] = DataObject::getSchema()->tableName($class);
322
            //     }
323
            // }
324
            // $this->tablesPerClassName[$this->object->ClassName] = array_unique($classTables);
325
326
            $srcQuery = DataList::create($this->object->ClassName)
327
                ->filter('ID', $this->object->ID)
328
                ->dataQuery()
329
                ->query()
330
            ;
331
            $this->tablesPerClassName[$this->object->ClassName] = $srcQuery->queriedTables();
332
        }
333
334
        return $this->tablesPerClassName[$this->object->ClassName];
335
    }
336
337
    protected function addCountRegister(string $tableName, int $count): void
338
    {
339
        $this->countPerTableRegister[$tableName] = $count;
340
    }
341
342
343
    protected function getCountPerTable(string $table) : int
344
    {
345
        $overallCount = $this->countPerTableRegister[$table] ?? -1;
346
        if($overallCount === -1) {
347
            $selectOverallCountSQL = '
348
                SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"';
349
            $overallCount = DB::query($selectOverallCountSQL)->value();
350
        }
351
352
        return $overallCount;
353
    }
354
355
}
356