Passed
Pull Request — 4 (#10237)
by Maxime
07:41
created

TempDatabase   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 106
c 0
b 0
f 0
dl 0
loc 308
rs 8.96
wmc 43

13 Methods

Rating   Name   Duplication   Size   Complexity  
A supportsTransactions() 0 3 1
A clearAllData() 0 18 4
A rollbackTransaction() 0 16 5
A isUsed() 0 4 1
A startTransaction() 0 4 2
A getConn() 0 3 1
A __construct() 0 3 1
A build() 0 32 5
A kill() 0 27 5
A deleteAll() 0 8 3
B rebuildTables() 0 43 7
A resetDBSchema() 0 24 5
A isDBTemp() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like TempDatabase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TempDatabase, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use Exception;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Environment;
9
use SilverStripe\Core\Injector\Injectable;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Dev\TestOnly;
12
use SilverStripe\ORM\DataExtension;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\DB;
15
16
class TempDatabase
17
{
18
    use Injectable;
19
20
    /**
21
     * Connection name
22
     *
23
     * @var string
24
     */
25
    protected $name = null;
26
27
    /**
28
     * Workaround to avoid infinite loops.
29
     *
30
     * @var Exception
31
     */
32
    private $skippedException = null;
33
34
    /**
35
     * Optionally remove the test DB when the PHP process exits
36
     *
37
     * @var boolean
38
     */
39
    private static $teardown_on_exit = true;
0 ignored issues
show
introduced by
The private property $teardown_on_exit is not used, and could be removed.
Loading history...
40
41
    /**
42
     * Create a new temp database
43
     *
44
     * @param string $name DB Connection name to use
45
     */
46
    public function __construct($name = 'default')
47
    {
48
        $this->name = $name;
49
    }
50
51
    /**
52
     * Check if the given name matches the temp_db pattern
53
     *
54
     * @param string $name
55
     * @return bool
56
     */
57
    protected function isDBTemp($name)
58
    {
59
        $prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
60
        $result = preg_match(
61
            sprintf('/^%stmpdb_[0-9]+_[0-9]+$/i', preg_quote($prefix, '/')),
62
            $name ?: ''
63
        );
64
        return $result === 1;
65
    }
66
67
    /**
68
     * @return Database
69
     */
70
    protected function getConn()
71
    {
72
        return DB::get_conn($this->name);
73
    }
74
75
    /**
76
     * Returns true if we are currently using a temporary database
77
     *
78
     * @return bool
79
     */
80
    public function isUsed()
81
    {
82
        $selected = $this->getConn()->getSelectedDatabase();
83
        return $this->isDBTemp($selected);
84
    }
85
86
    /**
87
     * @return bool
88
     */
89
    public function supportsTransactions()
90
    {
91
        return static::getConn()->supportsTransactions();
0 ignored issues
show
Bug Best Practice introduced by
The method SilverStripe\ORM\Connect\TempDatabase::getConn() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

91
        return static::/** @scrutinizer ignore-call */ getConn()->supportsTransactions();
Loading history...
92
    }
93
94
    /**
95
     * Start a transaction for easy rollback after tests
96
     */
97
    public function startTransaction()
98
    {
99
        if (static::getConn()->supportsTransactions()) {
0 ignored issues
show
Bug Best Practice introduced by
The method SilverStripe\ORM\Connect\TempDatabase::getConn() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

99
        if (static::/** @scrutinizer ignore-call */ getConn()->supportsTransactions()) {
Loading history...
100
            static::getConn()->transactionStart();
101
        }
102
    }
103
104
    /**
105
     * Rollback a transaction (or trash all data if the DB doesn't support databases
106
     *
107
     * @return bool True if successfully rolled back, false otherwise. On error the DB is
108
     * killed and must be re-created. Note that calling rollbackTransaction() when there
109
     * is no transaction is counted as a failure, user code should either kill or flush the DB
110
     * as necessary
111
     */
112
    public function rollbackTransaction()
113
    {
114
        // Ensure a rollback can be performed
115
        $success = static::getConn()->supportsTransactions()
0 ignored issues
show
Bug Best Practice introduced by
The method SilverStripe\ORM\Connect\TempDatabase::getConn() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
        $success = static::/** @scrutinizer ignore-call */ getConn()->supportsTransactions()
Loading history...
116
            && static::getConn()->transactionDepth();
117
        if (!$success) {
118
            return false;
119
        }
120
        try {
121
            // Explicit false = gnostic error from transactionRollback
122
            if (static::getConn()->transactionRollback() === false) {
123
                return false;
124
            }
125
            return true;
126
        } catch (DatabaseException $ex) {
127
            return false;
128
        }
129
    }
130
131
    /**
132
     * Destroy the current temp database
133
     */
134
    public function kill()
135
    {
136
        // Nothing to kill
137
        if (!$this->isUsed()) {
138
            return;
139
        }
140
141
        // Rollback any transactions (note: Success ignored)
142
        $this->rollbackTransaction();
143
144
        // Check the database actually exists
145
        $dbConn = $this->getConn();
146
        $dbName = $dbConn->getSelectedDatabase();
147
        if (!$dbConn->databaseExists($dbName)) {
148
            return;
149
        }
150
151
        // Some DataExtensions keep a static cache of information that needs to
152
        // be reset whenever the database is killed
153
        foreach (ClassInfo::subclassesFor(DataExtension::class) as $class) {
154
            $toCall = [$class, 'on_db_reset'];
155
            if (is_callable($toCall)) {
156
                call_user_func($toCall);
157
            }
158
        }
159
160
        $dbConn->dropSelectedDatabase();
161
    }
162
163
    /**
164
     * Remove all content from the temporary database.
165
     */
166
    public function clearAllData()
167
    {
168
        if (!$this->isUsed()) {
169
            return;
170
        }
171
172
        $this->getConn()->clearAllData();
173
174
        // Some DataExtensions keep a static cache of information that needs to
175
        // be reset whenever the database is cleaned out
176
        $classes = array_merge(
177
            ClassInfo::subclassesFor(DataExtension::class),
178
            ClassInfo::subclassesFor(DataObject::class)
179
        );
180
        foreach ($classes as $class) {
181
            $toCall = [$class, 'on_db_reset'];
182
            if (is_callable($toCall)) {
183
                call_user_func($toCall);
184
            }
185
        }
186
    }
187
188
    /**
189
     * Create temp DB without creating extra objects
190
     *
191
     * @return string DB name
192
     */
193
    public function build()
194
    {
195
        // Disable PHPUnit error handling
196
        $oldErrorHandler = set_error_handler(null);
197
198
        // Create a temporary database, and force the connection to use UTC for time
199
        $dbConn = $this->getConn();
200
        $prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
201
        do {
202
            $dbname = strtolower(sprintf('%stmpdb_%s_%s', $prefix, time(), rand(1000000, 9999999)));
203
        } while ($dbConn->databaseExists($dbname));
204
205
        $dbConn->selectDatabase($dbname, true);
206
207
        $this->resetDBSchema();
208
209
        // Reinstate PHPUnit error handling
210
        set_error_handler($oldErrorHandler);
211
212
        // Ensure test db is killed on exit
213
        $teardownOnExit = Config::inst()->get(static::class, 'teardown_on_exit');
214
        if ($teardownOnExit) {
215
            register_shutdown_function(function () {
216
                try {
217
                    $this->kill();
218
                } catch (Exception $ex) {
219
                    // An exception thrown while trying to remove a test database shouldn't fail a build, ignore
220
                }
221
            });
222
        }
223
224
        return $dbname;
225
    }
226
227
    /**
228
     * Rebuild all database tables
229
     *
230
     * @param array $extraDataObjects
231
     */
232
    protected function rebuildTables($extraDataObjects = [])
233
    {
234
        DataObject::reset();
235
236
        // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
237
        Injector::inst()->unregisterObjects(DataObject::class);
238
239
        $dataClasses = ClassInfo::subclassesFor(DataObject::class);
240
        array_shift($dataClasses);
241
242
        $oldCheckAndRepairOnBuild = Config::inst()->get(DBSchemaManager::class, 'check_and_repair_on_build');
243
        Config::modify()->set(DBSchemaManager::class, 'check_and_repair_on_build', false);
244
245
        $schema = $this->getConn()->getSchemaManager();
246
        $schema->quiet();
247
        $schema->schemaUpdate(
248
            function () use ($dataClasses, $extraDataObjects) {
249
                foreach ($dataClasses as $dataClass) {
250
                    // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
251
                    if (class_exists($dataClass)) {
252
                        $SNG = singleton($dataClass);
253
                        if (!($SNG instanceof TestOnly)) {
254
                            $SNG->requireTable();
255
                        }
256
                    }
257
                }
258
259
                // If we have additional dataobjects which need schema, do so here:
260
                if ($extraDataObjects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extraDataObjects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
261
                    foreach ($extraDataObjects as $dataClass) {
262
                        $SNG = singleton($dataClass);
263
                        if (singleton($dataClass) instanceof DataObject) {
264
                            $SNG->requireTable();
265
                        }
266
                    }
267
                }
268
            }
269
        );
270
271
        Config::modify()->set(DBSchemaManager::class, 'check_and_repair_on_build', $oldCheckAndRepairOnBuild);
272
273
        ClassInfo::reset_db_cache();
274
        DataObject::singleton()->flushCache();
275
    }
276
277
    /**
278
     * Clear all temp DBs on this connection
279
     *
280
     * Note: This will output results to stdout unless suppressOutput
281
     * is set on the current db schema
282
     */
283
    public function deleteAll()
284
    {
285
        $schema = $this->getConn()->getSchemaManager();
286
        foreach ($schema->databaseList() as $dbName) {
287
            if ($this->isDBTemp($dbName)) {
288
                $schema->dropDatabase($dbName);
289
                $schema->alterationMessage("Dropped database \"$dbName\"", 'deleted');
290
                flush();
291
            }
292
        }
293
    }
294
295
    /**
296
     * Reset the testing database's schema.
297
     *
298
     * @param array $extraDataObjects List of extra dataobjects to build
299
     */
300
    public function resetDBSchema(array $extraDataObjects = [])
301
    {
302
        // Skip if no DB
303
        if (!$this->isUsed()) {
304
            return;
305
        }
306
307
        try {
308
            $this->rebuildTables($extraDataObjects);
309
        } catch (DatabaseException $ex) {
310
            // Avoid infinite loops
311
            if ($this->skippedException && $this->skippedException->getMessage() == $ex->getMessage()) {
312
                throw $ex;
313
            }
314
315
            $this->skippedException = $ex;
316
317
            // In case of error during build force a hard reset
318
            // e.g. pgsql doesn't allow schema updates inside transactions
319
            $this->kill();
320
            $this->build();
321
            $this->rebuildTables($extraDataObjects);
322
323
            $this->skippedException = null;
324
        }
325
    }
326
}
327