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
|
|
|
* Create a new temp database |
28
|
|
|
* |
29
|
|
|
* @param string $name DB Connection name to use |
30
|
|
|
*/ |
31
|
|
|
public function __construct($name = 'default') |
32
|
|
|
{ |
33
|
|
|
$this->name = $name; |
34
|
|
|
} |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Check if the given name matches the temp_db pattern |
38
|
|
|
* |
39
|
|
|
* @param string $name |
40
|
|
|
* @return bool |
41
|
|
|
*/ |
42
|
|
|
protected function isDBTemp($name) |
43
|
|
|
{ |
44
|
|
|
$prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_'; |
45
|
|
|
$result = preg_match( |
46
|
|
|
sprintf('/^%stmpdb_[0-9]+_[0-9]+$/i', preg_quote($prefix, '/')), |
47
|
|
|
$name |
48
|
|
|
); |
49
|
|
|
return $result === 1; |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @return Database |
54
|
|
|
*/ |
55
|
|
|
protected function getConn() |
56
|
|
|
{ |
57
|
|
|
return DB::get_conn($this->name); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Returns true if we are currently using a temporary database |
62
|
|
|
* |
63
|
|
|
* @return bool |
64
|
|
|
*/ |
65
|
|
|
public function isUsed() |
66
|
|
|
{ |
67
|
|
|
$selected = $this->getConn()->getSelectedDatabase(); |
68
|
|
|
return $this->isDBTemp($selected); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Destroy the current temp database |
73
|
|
|
*/ |
74
|
|
|
public function kill() |
75
|
|
|
{ |
76
|
|
|
// Delete our temporary database |
77
|
|
|
if (!$this->isUsed()) { |
78
|
|
|
return; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
// Check the database actually exists |
82
|
|
|
$dbConn = $this->getConn(); |
83
|
|
|
$dbName = $dbConn->getSelectedDatabase(); |
84
|
|
|
if (!$dbConn->databaseExists($dbName)) { |
85
|
|
|
return; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
// Some DataExtensions keep a static cache of information that needs to |
89
|
|
|
// be reset whenever the database is killed |
90
|
|
|
foreach (ClassInfo::subclassesFor(DataExtension::class) as $class) { |
91
|
|
|
$toCall = array($class, 'on_db_reset'); |
92
|
|
|
if (is_callable($toCall)) { |
93
|
|
|
call_user_func($toCall); |
94
|
|
|
} |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
$dbConn->dropSelectedDatabase(); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Remove all content from the temporary database. |
102
|
|
|
*/ |
103
|
|
|
public function clearAllData() |
104
|
|
|
{ |
105
|
|
|
if (!$this->isUsed()) { |
106
|
|
|
return; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
$this->getConn()->clearAllData(); |
110
|
|
|
|
111
|
|
|
// Some DataExtensions keep a static cache of information that needs to |
112
|
|
|
// be reset whenever the database is cleaned out |
113
|
|
|
$classes = array_merge( |
114
|
|
|
ClassInfo::subclassesFor(DataExtension::class), |
115
|
|
|
ClassInfo::subclassesFor(DataObject::class) |
116
|
|
|
); |
117
|
|
|
foreach ($classes as $class) { |
118
|
|
|
$toCall = array($class, 'on_db_reset'); |
119
|
|
|
if (is_callable($toCall)) { |
120
|
|
|
call_user_func($toCall); |
121
|
|
|
} |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Create temp DB without creating extra objects |
127
|
|
|
* |
128
|
|
|
* @return string DB name |
129
|
|
|
*/ |
130
|
|
|
public function build() |
131
|
|
|
{ |
132
|
|
|
// Disable PHPUnit error handling |
133
|
|
|
$oldErrorHandler = set_error_handler(null); |
134
|
|
|
|
135
|
|
|
// Create a temporary database, and force the connection to use UTC for time |
136
|
|
|
$dbConn = $this->getConn(); |
137
|
|
|
$prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_'; |
138
|
|
|
do { |
139
|
|
|
$dbname = strtolower(sprintf('%stmpdb_%s_%s', $prefix, time(), rand(1000000, 9999999))); |
140
|
|
|
} while ($dbConn->databaseExists($dbname)); |
141
|
|
|
|
142
|
|
|
$dbConn->selectDatabase($dbname, true); |
143
|
|
|
|
144
|
|
|
$this->resetDBSchema(); |
145
|
|
|
|
146
|
|
|
// Reinstate PHPUnit error handling |
147
|
|
|
set_error_handler($oldErrorHandler); |
148
|
|
|
|
149
|
|
|
// Ensure test db is killed on exit |
150
|
|
|
register_shutdown_function(function () { |
151
|
|
|
try { |
152
|
|
|
$this->kill(); |
153
|
|
|
} catch (Exception $ex) { |
154
|
|
|
// An exception thrown while trying to remove a test database shouldn't fail a build, ignore |
155
|
|
|
} |
156
|
|
|
}); |
157
|
|
|
|
158
|
|
|
return $dbname; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Clear all temp DBs on this connection |
163
|
|
|
* |
164
|
|
|
* Note: This will output results to stdout unless suppressOutput |
165
|
|
|
* is set on the current db schema |
166
|
|
|
*/ |
167
|
|
|
public function deleteAll() |
168
|
|
|
{ |
169
|
|
|
$schema = $this->getConn()->getSchemaManager(); |
170
|
|
|
foreach ($schema->databaseList() as $dbName) { |
171
|
|
|
if ($this->isDBTemp($dbName)) { |
172
|
|
|
$schema->dropDatabase($dbName); |
173
|
|
|
$schema->alterationMessage("Dropped database \"$dbName\"", 'deleted'); |
174
|
|
|
flush(); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Reset the testing database's schema. |
181
|
|
|
* |
182
|
|
|
* @param array $extraDataObjects List of extra dataobjects to build |
183
|
|
|
*/ |
184
|
|
|
public function resetDBSchema(array $extraDataObjects = []) |
185
|
|
|
{ |
186
|
|
|
if (!$this->isUsed()) { |
187
|
|
|
return; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
DataObject::reset(); |
191
|
|
|
|
192
|
|
|
// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild() |
193
|
|
|
Injector::inst()->unregisterObjects(DataObject::class); |
194
|
|
|
|
195
|
|
|
$dataClasses = ClassInfo::subclassesFor(DataObject::class); |
196
|
|
|
array_shift($dataClasses); |
197
|
|
|
|
198
|
|
|
$schema = $this->getConn()->getSchemaManager(); |
199
|
|
|
$schema->quiet(); |
200
|
|
|
$schema->schemaUpdate(function () use ($dataClasses, $extraDataObjects) { |
201
|
|
|
foreach ($dataClasses as $dataClass) { |
202
|
|
|
// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness |
203
|
|
|
if (class_exists($dataClass)) { |
204
|
|
|
$SNG = singleton($dataClass); |
205
|
|
|
if (!($SNG instanceof TestOnly)) { |
206
|
|
|
$SNG->requireTable(); |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
// If we have additional dataobjects which need schema, do so here: |
212
|
|
|
if ($extraDataObjects) { |
|
|
|
|
213
|
|
|
foreach ($extraDataObjects as $dataClass) { |
214
|
|
|
$SNG = singleton($dataClass); |
215
|
|
|
if (singleton($dataClass) instanceof DataObject) { |
216
|
|
|
$SNG->requireTable(); |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
}); |
221
|
|
|
|
222
|
|
|
ClassInfo::reset_db_cache(); |
223
|
|
|
DataObject::singleton()->flushCache(); |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
|
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.