Passed
Pull Request — 4 (#8843)
by Daniel
08:11
created

TempDatabase::rebuildTables()   B

Complexity

Conditions 7
Paths 1

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 1
nop 1
dl 0
loc 36
rs 8.8333
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
     * 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) {
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...
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