Passed
Push — master ( affe5c...28600f )
by Nicolaas
02:36
created

RunForOneObject::findBestSuitedTemplates()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 8
nc 7
nop 0
dl 0
loc 17
rs 9.6111
c 2
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
     *
61
     * @var bool
62
     */
63
    protected $verbose = false;
64
65
    /**
66
     *
67
     * @var bool
68
     */
69
    protected $dryRun = false;
70
71
    /**
72
     * schema is:
73
     * ```php
74
     *     ClassName => [
75
     *         PruningTemplateClassName1 => [
76
     *             "PropertyName1" => Value1
77
     *             "PropertyName2" => Value2
78
     *         ],
79
     *         PruningTemplateClassName2 => [
80
     *         ],
81
     *     ]
82
     * ```.
83
     * N.B. least specific first!
84
     *
85
     * @var array
86
     */
87
    private static $templates = [
88
        'default' => [
89
            BasedOnTimeScale::class => [],
90
        ],
91
        SiteTree::class => [
92
            Drafts::class => [],
93
            SiteTreeVersioningTemplate::class => [],
94
        ],
95
        File::class => [
96
            DeleteFiles::class => [],
97
            BasedOnTimeScale::class => [],
98
        ],
99
    ];
100
101
    public function __construct()
102
    {
103
        $this->templatesAvailable = array_reverse(
104
            $this->Config()->get('templates'),
105
            true //important - to preserve keys!
106
        );
107
    }
108
109
    public static function inst()
110
    {
111
        return Injector::inst()->get(static::class);
112
    }
113
114
    public function setVerbose(?bool $verbose = true) : self
115
    {
116
        $this->verbose = $verbose;
117
        return $this;
118
    }
119
120
    public function setDryRun(?bool $dryRun = true) : self
121
    {
122
        $this->dryRun = $dryRun;
123
        return $this;
124
    }
125
126
    /**
127
     * returns the total number deleted.
128
     *
129
     * @param DataObject $object
130
     * @return int number of deletions
131
     */
132
    public function deleteSuperfluousVersions($object): int
133
    {
134
        $this->object = $object;
135
        if(! $this->isValidObject()) {
136
            return 0;
137
        }
138
        // array of version IDs to delete
139
        // IMPORTANT
140
        $this->toDelete[$this->getUniqueKey()] = [];
141
142
        // Base table has Versioned data
143
        $totalDeleted = 0;
144
145
        $myTemplates = $this->findBestSuitedTemplates();
146
        foreach ($myTemplates as $className => $options) {
147
            $runner = new $className($this->object, $this->toDelete[$this->getUniqueKey()]);
148
            if ($this->verbose) {
149
                DB::alteration_message('... ... ... Running ' . $runner->getTitle() . ': ' . $runner->getDescription());
150
            }
151
152
            foreach ($options as $key => $value) {
153
                $method = 'set' . $key;
154
                $runner->{$method}($value);
155
            }
156
157
            $runner->run();
158
            $this->toDelete[$this->getUniqueKey()] = $runner->getToDelete();
159
160
            if ($this->verbose) {
161
                DB::alteration_message('... ... ... total versions to delete now ' . count($this->toDelete[$this->getUniqueKey()]));
162
            }
163
        }
164
165
        if (! count($this->toDelete[$this->getUniqueKey()])) {
166
            return 0;
167
        }
168
169
        // Ugly (borrowed from DataObject::class), but returns all
170
        // database tables relating to DataObject
171
        $queriedTables = $this->getTablesForClassName();
172
        foreach ($queriedTables as $table) {
173
            if($this->dryRun === true) {
174
                $select = '
175
                    SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
176
                    WHERE
177
                        "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
178
                        AND "RecordID" = ' . (int) $this->object->ID;
179
180
                $value = DB::query($select)->value();
181
                if ($this->verbose) {
182
                    DB::alteration_message('... ... ... running ' . $select);
183
                    DB::alteration_message('... ... ... total rows to be deleted  ... ' . $value);
184
                }
185
            } else {
186
                $delSQL = '
187
                    DELETE FROM "' . $table . '_Versions"
188
                    WHERE
189
                        "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
190
                        AND "RecordID" = ' . (int) $this->object->ID;
191
192
                DB::query($delSQL);
193
                $totalDeleted += DB::affected_rows();
194
                if ($this->verbose) {
195
                    DB::alteration_message('... ... ... running ' . $delSQL);
196
                    DB::alteration_message('... ... ... total rows deleted ... ' . $totalDeleted);
197
                }
198
            }
199
        }
200
201
        return $totalDeleted;
202
    }
203
204
    /**
205
     * returns the total number deleted.
206
     *
207
     * @param DataObject $object
208
     * @param bool       $verbose
209
     */
210
    public function getTemplatesDescription($object): array
211
    {
212
        $array = [];
213
        $this->object = $object;
214
        if($this->isValidObject()) {
215
            $myTemplates = $this->findBestSuitedTemplates();
216
            foreach ($myTemplates as $className => $options) {
217
                $runner = new $className($this->object, []);
218
                $array[] =  $runner->getTitle() . ': ' . $runner->getDescription();
219
            }
220
        }
221
222
        return $array;
223
    }
224
225
    /**
226
     * we use this to make sure we never mix up two records.
227
     */
228
    protected function getUniqueKey(): string
229
    {
230
        return $this->object->ClassName . '_' . $this->object->ID;
231
    }
232
233
    protected function hasStages(): bool
234
    {
235
        $hasStages = false;
236
        if($this->object->hasMethod('hasStages')) {
237
            $oldMode = Versioned::get_reading_mode();
238
            if ('Stage.Stage' !== $oldMode) {
239
                Versioned::set_reading_mode('Stage.Stage');
240
            }
241
242
            $hasStages = (bool) $this->object->hasStages();
243
            if ('Stage.Stage' !== $oldMode) {
244
                Versioned::set_reading_mode($oldMode);
245
            }
246
247
        }
248
        return $hasStages;
249
    }
250
251
    protected function findBestSuitedTemplates()
252
    {
253
        if (empty($this->templatesPerClassName[$this->object->ClassName])) {
254
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
255
                if (is_a($this->object, $className)) {
256
                    $this->templatesPerClassName[$this->object->ClassName] = $classesWithOptions;
257
258
                    break;
259
                }
260
            }
261
262
            if (! isset($this->templatesPerClassName[$this->object->ClassName])) {
263
                $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 254. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
264
            }
265
        }
266
267
        return $this->templatesPerClassName[$this->object->ClassName];
268
    }
269
270
    protected function isValidObject() : bool
271
    {
272
        if (false === $this->hasStages()) {
273
            if ($this->verbose) {
274
                DB::alteration_message('... ... ... Error, no stages', 'deleted');
275
            }
276
277
            return false;
278
        }
279
280
        if(! $this->object->hasMethod('isLiveVersion')) {
281
            return false;
282
        }
283
        if (false === $this->object->isLiveVersion()) {
284
            if ($this->verbose) {
285
                DB::alteration_message('... ... ... Error, not a live version', 'deleted');
286
            }
287
288
            return false;
289
        }
290
        return $this->object && $this->object->exists();
291
292
    }
293
294
    protected function getTablesForClassName(): array
295
    {
296
        if (empty($this->tablesPerClassName[$this->object->ClassName])) {
297
            $srcQuery = DataList::create($this->object->ClassName)
298
                ->filter('ID', $this->object->ID)
299
                ->dataQuery()
300
                ->query()
301
            ;
302
            $this->tablesPerClassName[$this->object->ClassName] = $srcQuery->queriedTables();
303
        }
304
305
        return $this->tablesPerClassName[$this->object->ClassName];
306
    }
307
}
308