Passed
Push — 4 ( 77a45c...ec956a )
by Damian
07:50 queued 13s
created

TempDatabase::startTransaction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use Exception;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Environment;
8
use SilverStripe\Core\Injector\Injectable;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\Dev\TestOnly;
11
use SilverStripe\ORM\DataExtension;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\DB;
14
15
class TempDatabase
16
{
17
    use Injectable;
18
19
    /**
20
     * Connection name
21
     *
22
     * @var string
23
     */
24
    protected $name = null;
25
26
    /**
27
     * @var bool If a transaction has been started
28
     */
29
    protected $hasStarted = false;
30
31
    /**
32
     * Create a new temp database
33
     *
34
     * @param string $name DB Connection name to use
35
     */
36
    public function __construct($name = 'default')
37
    {
38
        $this->name = $name;
39
    }
40
41
    /**
42
     * Check if the given name matches the temp_db pattern
43
     *
44
     * @param string $name
45
     * @return bool
46
     */
47
    protected function isDBTemp($name)
48
    {
49
        $prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
50
        $result = preg_match(
51
            sprintf('/^%stmpdb_[0-9]+_[0-9]+$/i', preg_quote($prefix, '/')),
52
            $name
53
        );
54
        return $result === 1;
55
    }
56
57
    /**
58
     * @return Database
59
     */
60
    protected function getConn()
61
    {
62
        return DB::get_conn($this->name);
63
    }
64
65
    /**
66
     * Returns true if we are currently using a temporary database
67
     *
68
     * @return bool
69
     */
70
    public function isUsed()
71
    {
72
        $selected = $this->getConn()->getSelectedDatabase();
73
        return $this->isDBTemp($selected);
74
    }
75
76
    /**
77
     * @return bool
78
     */
79
    public function hasStarted()
80
    {
81
        return $this->hasStarted;
82
    }
83
84
    /**
85
     * @return bool
86
     */
87
    public function supportsTransactions()
88
    {
89
        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

89
        return static::/** @scrutinizer ignore-call */ getConn()->supportsTransactions();
Loading history...
90
    }
91
92
    /**
93
     * Start a transaction for easy rollback after tests
94
     */
95
    public function startTransaction()
96
    {
97
        $this->hasStarted = true;
98
        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

98
        if (static::/** @scrutinizer ignore-call */ getConn()->supportsTransactions()) {
Loading history...
99
            static::getConn()->transactionStart();
100
        }
101
    }
102
103
    /**
104
     * Rollback a transaction (or trash all data if the DB doesn't support databases
105
     */
106
    public function rollbackTransaction()
107
    {
108
        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

108
        if (static::/** @scrutinizer ignore-call */ getConn()->supportsTransactions()) {
Loading history...
109
            static::getConn()->transactionRollback();
110
        } else {
111
            $this->hasStarted = false;
112
            static::clearAllData();
0 ignored issues
show
Bug Best Practice introduced by
The method SilverStripe\ORM\Connect...atabase::clearAllData() 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

112
            static::/** @scrutinizer ignore-call */ 
113
                    clearAllData();
Loading history...
113
        }
114
    }
115
116
    /**
117
     * Destroy the current temp database
118
     */
119
    public function kill()
120
    {
121
        // Delete our temporary database
122
        if (!$this->isUsed()) {
123
            return;
124
        }
125
126
        // Check the database actually exists
127
        $dbConn = $this->getConn();
128
        $dbName = $dbConn->getSelectedDatabase();
129
        if (!$dbConn->databaseExists($dbName)) {
130
            return;
131
        }
132
133
        // Some DataExtensions keep a static cache of information that needs to
134
        // be reset whenever the database is killed
135
        foreach (ClassInfo::subclassesFor(DataExtension::class) as $class) {
136
            $toCall = array($class, 'on_db_reset');
137
            if (is_callable($toCall)) {
138
                call_user_func($toCall);
139
            }
140
        }
141
142
        $dbConn->dropSelectedDatabase();
143
    }
144
145
    /**
146
     * Remove all content from the temporary database.
147
     */
148
    public function clearAllData()
149
    {
150
        $this->hasStarted = false;
151
        if (!$this->isUsed()) {
152
            return;
153
        }
154
155
        $this->getConn()->clearAllData();
156
157
        // Some DataExtensions keep a static cache of information that needs to
158
        // be reset whenever the database is cleaned out
159
        $classes = array_merge(
160
            ClassInfo::subclassesFor(DataExtension::class),
161
            ClassInfo::subclassesFor(DataObject::class)
162
        );
163
        foreach ($classes as $class) {
164
            $toCall = array($class, 'on_db_reset');
165
            if (is_callable($toCall)) {
166
                call_user_func($toCall);
167
            }
168
        }
169
    }
170
171
    /**
172
     * Create temp DB without creating extra objects
173
     *
174
     * @return string DB name
175
     */
176
    public function build()
177
    {
178
        // Disable PHPUnit error handling
179
        $oldErrorHandler = set_error_handler(null);
180
181
        // Create a temporary database, and force the connection to use UTC for time
182
        $dbConn = $this->getConn();
183
        $prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
184
        do {
185
            $dbname = strtolower(sprintf('%stmpdb_%s_%s', $prefix, time(), rand(1000000, 9999999)));
186
        } while ($dbConn->databaseExists($dbname));
187
188
        $dbConn->selectDatabase($dbname, true);
189
190
        $this->resetDBSchema();
191
192
        // Reinstate PHPUnit error handling
193
        set_error_handler($oldErrorHandler);
194
195
        // Ensure test db is killed on exit
196
        register_shutdown_function(function () {
197
            try {
198
                $this->kill();
199
            } catch (Exception $ex) {
200
                // An exception thrown while trying to remove a test database shouldn't fail a build, ignore
201
            }
202
        });
203
204
        return $dbname;
205
    }
206
207
    /**
208
     * Clear all temp DBs on this connection
209
     *
210
     * Note: This will output results to stdout unless suppressOutput
211
     * is set on the current db schema
212
     */
213
    public function deleteAll()
214
    {
215
        $schema = $this->getConn()->getSchemaManager();
216
        foreach ($schema->databaseList() as $dbName) {
217
            if ($this->isDBTemp($dbName)) {
218
                $schema->dropDatabase($dbName);
219
                $schema->alterationMessage("Dropped database \"$dbName\"", 'deleted');
220
                flush();
221
            }
222
        }
223
    }
224
225
    /**
226
     * Reset the testing database's schema.
227
     *
228
     * @param array $extraDataObjects List of extra dataobjects to build
229
     */
230
    public function resetDBSchema(array $extraDataObjects = [])
231
    {
232
        // pgsql doesn't allow schema updates inside transactions
233
        // so we need to rollback any transactions before commencing a schema reset
234
        if ($this->hasStarted()) {
235
            $this->rollbackTransaction();
236
        }
237
        if (!$this->isUsed()) {
238
            return;
239
        }
240
241
        DataObject::reset();
242
243
        // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
244
        Injector::inst()->unregisterObjects(DataObject::class);
245
246
        $dataClasses = ClassInfo::subclassesFor(DataObject::class);
247
        array_shift($dataClasses);
248
249
        $schema = $this->getConn()->getSchemaManager();
250
        $schema->quiet();
251
        $schema->schemaUpdate(function () use ($dataClasses, $extraDataObjects) {
252
            foreach ($dataClasses as $dataClass) {
253
                // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
254
                if (class_exists($dataClass)) {
255
                    $SNG = singleton($dataClass);
256
                    if (!($SNG instanceof TestOnly)) {
257
                        $SNG->requireTable();
258
                    }
259
                }
260
            }
261
262
            // If we have additional dataobjects which need schema, do so here:
263
            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...
264
                foreach ($extraDataObjects as $dataClass) {
265
                    $SNG = singleton($dataClass);
266
                    if (singleton($dataClass) instanceof DataObject) {
267
                        $SNG->requireTable();
268
                    }
269
                }
270
            }
271
        });
272
273
        ClassInfo::reset_db_cache();
274
        DataObject::singleton()->flushCache();
275
    }
276
}
277