Passed
Push — master ( cd075b...9f2e17 )
by Nicolaas
01:56
created

RunForOneObject   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 84
dl 0
loc 220
rs 10
c 7
b 0
f 0
wmc 26
1
<?php
2
3
namespace Sunnysideup\VersionPruner\Api;
4
5
use SilverStripe\Assets\File;
6
use SilverStripe\CMS\Model\SiteTree;
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;
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 setDry(?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 ' . $value);
197
                }
198
199
                $totalDeleted += DB::affected_rows();
200
                if ($this->verbose) {
201
                    DB::alteration_message('... ... ... total deleted now ... ' . $totalDeleted);
202
                }
203
            }
204
            } else {
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_ELSE on line 204 at column 14
Loading history...
205
                $delSQL = '
206
                    DELETE FROM "' . $table . '_Versions"
207
                    WHERE
208
                        "Version" IN (' . implode(',', $this->toDelete[$this->getUniqueKey()]) . ')
209
                        AND "RecordID" = ' . (int) $this->object->ID;
210
211
                DB::query($delSQL);
212
                if ($this->verbose) {
213
                    DB::alteration_message('... ... ... running ' . $delSQL);
214
                }
215
216
                $totalDeleted += DB::affected_rows();
217
                if ($this->verbose) {
218
                    DB::alteration_message('... ... ... total deleted now ... ' . $totalDeleted);
219
                }
220
            }
221
        }
222
223
        return $totalDeleted;
224
    }
225
226
    /**
227
     * we use this to make sure we never mix up two records.
228
     */
229
    protected function getUniqueKey(): string
230
    {
231
        return $this->object->ClassName . '_' . $this->object->ID;
232
    }
233
234
    protected function hasStages(): bool
235
    {
236
        $oldMode = Versioned::get_reading_mode();
237
        if ('Stage.Stage' !== $oldMode) {
238
            Versioned::set_reading_mode('Stage.Stage');
239
        }
240
241
        $hasStages = (bool) $this->object->hasStages();
242
        if ('Stage.Stage' !== $oldMode) {
243
            Versioned::set_reading_mode($oldMode);
244
        }
245
246
        return $hasStages;
247
    }
248
249
    protected function findBestSuitedTemplates()
250
    {
251
        if (empty($this->templatesPerClassName[$this->object->ClassName])) {
252
            foreach ($this->templatesAvailable as $className => $classesWithOptions) {
253
                if (is_a($this->object, $className)) {
254
                    $this->templatesPerClassName[$this->object->ClassName] = $classesWithOptions;
255
256
                    break;
257
                }
258
            }
259
260
            if (! isset($this->templatesPerClassName[$this->object->ClassName])) {
261
                $this->templatesPerClassName[$this->object->ClassName] = $templates['default'] ?? $classesWithOptions;
262
            }
263
        }
264
265
        return $this->templatesPerClassName[$this->object->ClassName];
266
    }
267
268
    protected function getTablesForClassName(): array
269
    {
270
        if (empty($this->tablesPerClassName[$this->object->ClassName])) {
271
            $srcQuery = DataList::create($this->object->ClassName)
272
                ->filter('ID', $this->object->ID)
273
                ->dataQuery()
274
                ->query()
275
            ;
276
            $this->tablesPerClassName[$this->object->ClassName] = $srcQuery->queriedTables();
277
        }
278
279
        return $this->tablesPerClassName[$this->object->ClassName];
280
    }
281
}
282