Passed
Push — master ( aef744...57a6a5 )
by Nicolaas
02:03
created

RunForOneObject::getTablesForClassName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 12
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
     *
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
     * @param bool       $verbose
131
     */
132
    public function deleteSuperfluousVersions($object, ?bool $verbose = false): int
133
    {
134
        $this->object = $object;
135
        $this->verbose = $verbose;
136
        if (false === $this->hasStages()) {
137
            if ($this->verbose) {
138
                DB::alteration_message('... ... ... Error, no stages', 'deleted');
139
            }
140
141
            return 0;
142
        }
143
144
        if (false === $this->object->isLiveVersion()) {
145
            if ($this->verbose) {
146
                DB::alteration_message('... ... ... Error, not a live version', 'deleted');
147
            }
148
149
            return 0;
150
        }
151
152
        // array of version IDs to delete
153
        // IMPORTANT
154
        $this->toDelete[$this->getUniqueKey()] = [];
155
156
        // Base table has Versioned data
157
        $totalDeleted = 0;
158
159
        $myTemplates = $this->findBestSuitedTemplates();
160
        foreach ($myTemplates as $className => $options) {
161
            $runner = new $className($this->object, $this->toDelete[$this->getUniqueKey()]);
162
            if ($this->verbose) {
163
                DB::alteration_message('... ... ... Running ' . $runner->getTitle() . ': ' . $runner->getDescription());
164
            }
165
166
            foreach ($options as $key => $value) {
167
                $method = 'set' . $key;
168
                $runner->{$method}($value);
169
            }
170
171
            $runner->run();
172
            $this->toDelete[$this->getUniqueKey()] = $runner->getToDelete();
173
174
            if ($this->verbose) {
175
                DB::alteration_message('... ... ... total versions to delete now ' . count($this->toDelete[$this->getUniqueKey()]));
176
            }
177
        }
178
179
        if (! count($this->toDelete[$this->getUniqueKey()])) {
180
            return 0;
181
        }
182
183
        // Ugly (borrowed from DataObject::class), but returns all
184
        // database tables relating to DataObject
185
        $queriedTables = $this->getTablesForClassName();
186
        foreach ($queriedTables as $table) {
187
            if($this->dryRun === true) {
188
                $select = '
189
                    SELECT COUNT(ID) AS C FROM "' . $table . '_Versions"
190
                    WHERE
191
                        "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
192
                        AND "RecordID" = ' . (int) $this->object->ID;
193
194
                $value = DB::query($select)->value();
195
                if ($this->verbose) {
196
                    DB::alteration_message('... ... ... running ' . $select);
197
                    DB::alteration_message('... ... ... total rows to be deleted  ... ' . $value);
198
                }
199
            } else {
200
                $delSQL = '
201
                    DELETE FROM "' . $table . '_Versions"
202
                    WHERE
203
                        "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
204
                        AND "RecordID" = ' . (int) $this->object->ID;
205
206
                DB::query($delSQL);
207
                $totalDeleted += DB::affected_rows();
208
                if ($this->verbose) {
209
                    DB::alteration_message('... ... ... running ' . $delSQL);
210
                    DB::alteration_message('... ... ... total rows deleted ... ' . $totalDeleted);
211
                }
212
            }
213
        }
214
215
        return $totalDeleted;
216
    }
217
218
    /**
219
     * we use this to make sure we never mix up two records.
220
     */
221
    protected function getUniqueKey(): string
222
    {
223
        return $this->object->ClassName . '_' . $this->object->ID;
224
    }
225
226
    protected function hasStages(): bool
227
    {
228
        $oldMode = Versioned::get_reading_mode();
229
        if ('Stage.Stage' !== $oldMode) {
230
            Versioned::set_reading_mode('Stage.Stage');
231
        }
232
233
        $hasStages = (bool) $this->object->hasStages();
234
        if ('Stage.Stage' !== $oldMode) {
235
            Versioned::set_reading_mode($oldMode);
236
        }
237
238
        return $hasStages;
239
    }
240
241
    protected function findBestSuitedTemplates()
242
    {
243
        if (empty($this->templatesPerClassName[$this->object->ClassName])) {
244
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
245
                if (is_a($this->object, $className)) {
246
                    $this->templatesPerClassName[$this->object->ClassName] = $classesWithOptions;
247
248
                    break;
249
                }
250
            }
251
252
            if (! isset($this->templatesPerClassName[$this->object->ClassName])) {
253
                $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 244. 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...
254
            }
255
        }
256
257
        return $this->templatesPerClassName[$this->object->ClassName];
258
    }
259
260
    protected function getTablesForClassName(): array
261
    {
262
        if (empty($this->tablesPerClassName[$this->object->ClassName])) {
263
            $srcQuery = DataList::create($this->object->ClassName)
264
                ->filter('ID', $this->object->ID)
265
                ->dataQuery()
266
                ->query()
267
            ;
268
            $this->tablesPerClassName[$this->object->ClassName] = $srcQuery->queriedTables();
269
        }
270
271
        return $this->tablesPerClassName[$this->object->ClassName];
272
    }
273
}
274