Passed
Pull Request — 4 (#10041)
by Guy
07:40
created

DB::forceDatabaseIsReady()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use InvalidArgumentException;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Convert;
12
use SilverStripe\Core\Environment;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\Dev\Deprecation;
15
use SilverStripe\ORM\Connect\Database;
16
use SilverStripe\ORM\Connect\DBConnector;
17
use SilverStripe\ORM\Connect\DBSchemaManager;
18
use SilverStripe\ORM\Connect\Query;
19
use SilverStripe\ORM\Queries\SQLExpression;
20
21
/**
22
 * Global database interface, complete with static methods.
23
 * Use this class for interacting with the database.
24
 */
25
class DB
26
{
27
28
    /**
29
     * This constant was added in SilverStripe 2.4 to indicate that SQL-queries
30
     * should now use ANSI-compatible syntax.  The most notable affect of this
31
     * change is that table and field names should be escaped with double quotes
32
     * and not backticks
33
     */
34
    const USE_ANSI_SQL = true;
35
36
    /**
37
     * Session key for alternative database name
38
     */
39
    const ALT_DB_KEY = 'alternativeDatabaseName';
40
41
    /**
42
     * Allow alternative DB to be disabled.
43
     * Necessary for DB backed session store to work.
44
     *
45
     * @config
46
     * @var bool
47
     */
48
    private static $alternative_database_enabled = true;
0 ignored issues
show
introduced by
The private property $alternative_database_enabled is not used, and could be removed.
Loading history...
49
50
    /**
51
     * The global database connection.
52
     *
53
     * @var Database
54
     */
55
    protected static $connections = [];
56
57
    /**
58
     * List of configurations for each connection
59
     *
60
     * @var array List of configs each in the $databaseConfig format
61
     */
62
    protected static $configs = [];
63
64
    /**
65
     * Array of classes that have been confirmed ready for database queries.
66
     * When the database has once been verified as ready, it will not do the
67
     * checks again.
68
     *
69
     * @var boolean[]
70
     */
71
    protected static $databaseReadyClasses = [];
72
73
    /**
74
     * The last SQL query run.
75
     * @var string
76
     */
77
    public static $lastQuery;
78
79
    /**
80
     * Internal flag to keep track of when db connection was attempted.
81
     */
82
    private static $connection_attempted = false;
83
84
    /**
85
     * Set the global database connection.
86
     * Pass an object that's a subclass of SS_Database.  This object will be used when {@link DB::query()}
87
     * is called.
88
     *
89
     * @param Database $connection The connection object to set as the connection.
90
     * @param string $name The name to give to this connection.  If you omit this argument, the connection
91
     * will be the default one used by the ORM.  However, you can store other named connections to
92
     * be accessed through DB::get_conn($name).  This is useful when you have an application that
93
     * needs to connect to more than one database.
94
     */
95
    public static function set_conn(Database $connection, $name = 'default')
96
    {
97
        self::$connections[$name] = $connection;
98
    }
99
100
    /**
101
     * Get the global database connection.
102
     *
103
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
104
     * the default connection is returned.
105
     * @return Database
106
     */
107
    public static function get_conn($name = 'default')
108
    {
109
        if (isset(self::$connections[$name])) {
110
            return self::$connections[$name];
111
        }
112
113
        // lazy connect
114
        $config = static::getConfig($name);
115
        if ($config) {
116
            return static::connect($config, $name);
117
        }
118
119
        return null;
120
    }
121
122
    /**
123
     * @deprecated since version 4.0 Use DB::get_conn instead
124
     * @todo PSR-2 standardisation will probably un-deprecate this
125
     */
126
    public static function getConn($name = 'default')
127
    {
128
        Deprecation::notice('4.0', 'Use DB::get_conn instead');
129
        return self::get_conn($name);
130
    }
131
132
    /**
133
     * Retrieves the schema manager for the current database
134
     *
135
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
136
     * the default connection is returned.
137
     * @return DBSchemaManager
138
     */
139
    public static function get_schema($name = 'default')
140
    {
141
        $connection = self::get_conn($name);
142
        if ($connection) {
0 ignored issues
show
introduced by
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
143
            return $connection->getSchemaManager();
144
        }
145
        return null;
146
    }
147
148
    /**
149
     * Builds a sql query with the specified connection
150
     *
151
     * @param SQLExpression $expression The expression object to build from
152
     * @param array $parameters Out parameter for the resulting query parameters
153
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
154
     * the default connection is returned.
155
     * @return string The resulting SQL as a string
156
     */
157
    public static function build_sql(SQLExpression $expression, &$parameters, $name = 'default')
158
    {
159
        $connection = self::get_conn($name);
160
        if ($connection) {
0 ignored issues
show
introduced by
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
161
            return $connection->getQueryBuilder()->buildSQL($expression, $parameters);
162
        } else {
163
            $parameters = [];
164
            return null;
165
        }
166
    }
167
168
    /**
169
     * Retrieves the connector object for the current database
170
     *
171
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
172
     * the default connection is returned.
173
     * @return DBConnector
174
     */
175
    public static function get_connector($name = 'default')
176
    {
177
        $connection = self::get_conn($name);
178
        if ($connection) {
0 ignored issues
show
introduced by
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
179
            return $connection->getConnector();
180
        }
181
        return null;
182
    }
183
184
    /**
185
     * Set an alternative database in a browser cookie,
186
     * with the cookie lifetime set to the browser session.
187
     * This is useful for integration testing on temporary databases.
188
     *
189
     * There is a strict naming convention for temporary databases to avoid abuse:
190
     * <prefix> (default: 'ss_') + tmpdb + <7 digits>
191
     * As an additional security measure, temporary databases will
192
     * be ignored in "live" mode.
193
     *
194
     * Note that the database will be set on the next request.
195
     * Set it to null to revert to the main database.
196
     *
197
     * @param string $name
198
     */
199
    public static function set_alternative_database_name($name = null)
200
    {
201
        // Ignore if disabled
202
        if (!Config::inst()->get(static::class, 'alternative_database_enabled')) {
203
            return;
204
        }
205
        // Skip if CLI
206
        if (Director::is_cli()) {
207
            return;
208
        }
209
        // Validate name
210
        if ($name && !self::valid_alternative_database_name($name)) {
211
            throw new InvalidArgumentException(sprintf(
212
                'Invalid alternative database name: "%s"',
213
                $name
214
            ));
215
        }
216
217
        // Set against session
218
        if (!Injector::inst()->has(HTTPRequest::class)) {
219
            return;
220
        }
221
        /** @var HTTPRequest $request */
222
        $request = Injector::inst()->get(HTTPRequest::class);
223
        if ($name) {
224
            $request->getSession()->set(self::ALT_DB_KEY, $name);
225
        } else {
226
            $request->getSession()->clear(self::ALT_DB_KEY);
227
        }
228
    }
229
230
    /**
231
     * Get the name of the database in use
232
     *
233
     * @return string|false Name of temp database, or false if not set
234
     */
235
    public static function get_alternative_database_name()
236
    {
237
        // Ignore if disabled
238
        if (!Config::inst()->get(static::class, 'alternative_database_enabled')) {
239
            return false;
240
        }
241
        // Skip if CLI
242
        if (Director::is_cli()) {
243
            return false;
244
        }
245
        // Skip if there's no request object yet
246
        if (!Injector::inst()->has(HTTPRequest::class)) {
247
            return null;
248
        }
249
        /** @var HTTPRequest $request */
250
        $request = Injector::inst()->get(HTTPRequest::class);
251
        // Skip if the session hasn't been started
252
        if (!$request->getSession()->isStarted()) {
253
            return null;
254
        }
255
256
        $name = $request->getSession()->get(self::ALT_DB_KEY);
257
        if (self::valid_alternative_database_name($name)) {
258
            return $name;
259
        }
260
261
        return false;
262
    }
263
264
    /**
265
     * Determines if the name is valid, as a security
266
     * measure against setting arbitrary databases.
267
     *
268
     * @param string $name
269
     * @return bool
270
     */
271
    public static function valid_alternative_database_name($name)
272
    {
273
        if (Director::isLive() || empty($name)) {
274
            return false;
275
        }
276
277
        $prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
278
        $pattern = strtolower(sprintf('/^%stmpdb\d{7}$/', $prefix));
279
        return (bool)preg_match($pattern, $name);
280
    }
281
282
    /**
283
     * Specify connection to a database
284
     *
285
     * Given the database configuration, this method will create the correct
286
     * subclass of {@link SS_Database}.
287
     *
288
     * @param array $databaseConfig A map of options. The 'type' is the name of the
289
     * subclass of SS_Database to use. For the rest of the options, see the specific class.
290
     * @param string $label identifier for the connection
291
     * @return Database
292
     */
293
    public static function connect($databaseConfig, $label = 'default')
294
    {
295
        // This is used by the "testsession" module to test up a test session using an alternative name
296
        if ($name = self::get_alternative_database_name()) {
297
            $databaseConfig['database'] = $name;
298
        }
299
300
        if (!isset($databaseConfig['type']) || empty($databaseConfig['type'])) {
301
            throw new InvalidArgumentException("DB::connect: Not passed a valid database config");
302
        }
303
304
        self::$connection_attempted = true;
305
306
        $dbClass = $databaseConfig['type'];
307
308
        // Using Injector->create allows us to use registered configurations
309
        // which may or may not map to explicit objects
310
        $conn = Injector::inst()->create($dbClass);
311
        self::set_conn($conn, $label);
312
        $conn->connect($databaseConfig);
313
314
        return $conn;
315
    }
316
317
    /**
318
     * Set config for a lazy-connected database
319
     *
320
     * @param array $databaseConfig
321
     * @param string $name
322
     */
323
    public static function setConfig($databaseConfig, $name = 'default')
324
    {
325
        static::$configs[$name] = $databaseConfig;
326
    }
327
328
    /**
329
     * Get the named connection config
330
     *
331
     * @param string $name
332
     * @return mixed
333
     */
334
    public static function getConfig($name = 'default')
335
    {
336
        if (isset(static::$configs[$name])) {
337
            return static::$configs[$name];
338
        }
339
    }
340
341
    /**
342
     * Returns true if a database connection has been attempted.
343
     * In particular, it lets the caller know if we're still so early in the execution pipeline that
344
     * we haven't even tried to connect to the database yet.
345
     */
346
    public static function connection_attempted()
347
    {
348
        return self::$connection_attempted;
349
    }
350
351
    /**
352
     * Execute the given SQL query.
353
     * @param string $sql The SQL query to execute
354
     * @param int $errorLevel The level of error reporting to enable for the query
355
     * @return Query
356
     */
357
    public static function query($sql, $errorLevel = E_USER_ERROR)
358
    {
359
        self::$lastQuery = $sql;
360
361
        return self::get_conn()->query($sql, $errorLevel);
362
    }
363
364
    /**
365
     * Helper function for generating a list of parameter placeholders for the
366
     * given argument(s)
367
     *
368
     * @param array|integer $input An array of items needing placeholders, or a
369
     * number to specify the number of placeholders
370
     * @param string $join The string to join each placeholder together with
371
     * @return string|null Either a list of placeholders, or null
372
     */
373
    public static function placeholders($input, $join = ', ')
374
    {
375
        if (is_array($input)) {
376
            $number = count($input);
377
        } elseif (is_numeric($input)) {
0 ignored issues
show
introduced by
The condition is_numeric($input) is always true.
Loading history...
378
            $number = intval($input);
379
        } else {
380
            return null;
381
        }
382
        if ($number === 0) {
383
            return null;
384
        }
385
        return implode($join, array_fill(0, $number, '?'));
386
    }
387
388
    /**
389
     * @param string $sql The parameterised query
390
     * @param array $parameters The parameters to inject into the query
391
     *
392
     * @return string
393
     */
394
    public static function inline_parameters($sql, $parameters)
395
    {
396
        $segments = preg_split('/\?/', $sql);
397
        $joined = '';
398
        $inString = false;
399
        $numSegments = count($segments);
400
        for ($i = 0; $i < $numSegments; $i++) {
401
            $input = $segments[$i];
402
            // Append next segment
403
            $joined .= $segments[$i];
404
            // Don't add placeholder after last segment
405
            if ($i === $numSegments - 1) {
406
                break;
407
            }
408
            // check string escape on previous fragment
409
            // Remove escaped backslashes, count them!
410
            $input = preg_replace('/\\\\\\\\/', '', $input);
411
            // Count quotes
412
            $totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes
413
            $escapedQuotes = substr_count($input, "\\'");
414
            if ((($totalQuotes - $escapedQuotes) % 2) !== 0) {
415
                $inString = !$inString;
0 ignored issues
show
introduced by
The condition $inString is always false.
Loading history...
416
            }
417
            // Append placeholder replacement
418
            if ($inString) {
419
                // Literal question mark
420
                $joined .= '?';
421
                continue;
422
            }
423
424
            // Encode and insert next parameter
425
            $next = array_shift($parameters);
426
            if (is_array($next) && isset($next['value'])) {
427
                $next = $next['value'];
428
            }
429
            if (is_bool($next)) {
430
                $value = $next ? '1' : '0';
431
            } elseif (is_int($next)) {
432
                $value = $next;
433
            } else {
434
                $value = (DB::get_conn() !== null) ? Convert::raw2sql($next, true) : $next;
435
            }
436
            $joined .= $value;
437
        }
438
        return $joined;
439
    }
440
441
    /**
442
     * Execute the given SQL parameterised query with the specified arguments
443
     *
444
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
445
     * @param array $parameters An ordered list of arguments.
446
     * @param int $errorLevel The level of error reporting to enable for the query
447
     * @return Query
448
     */
449
    public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR)
450
    {
451
        self::$lastQuery = $sql;
452
453
        return self::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
454
    }
455
456
    /**
457
     * Execute a complex manipulation on the database.
458
     * A manipulation is an array of insert / or update sequences.  The keys of the array are table names,
459
     * and the values are map containing 'command' and 'fields'.  Command should be 'insert' or 'update',
460
     * and fields should be a map of field names to field values, including quotes.  The field value can
461
     * also be a SQL function or similar.
462
     *
463
     * Example:
464
     * <code>
465
     * array(
466
     *   // Command: insert
467
     *   "table name" => array(
468
     *      "command" => "insert",
469
     *      "fields" => array(
470
     *         "ClassName" => "'MyClass'", // if you're setting a literal, you need to escape and provide quotes
471
     *         "Created" => "now()", // alternatively, you can call DB functions
472
     *         "ID" => 234,
473
     *       ),
474
     *      "id" => 234 // an alternative to providing ID in the fields list
475
     *    ),
476
     *
477
     *   // Command: update
478
     *   "other table" => array(
479
     *      "command" => "update",
480
     *      "fields" => array(
481
     *         "ClassName" => "'MyClass'",
482
     *         "LastEdited" => "now()",
483
     *       ),
484
     *      "where" => "ID = 234",
485
     *      "id" => 234 // an alternative to providing a where clause
486
     *    ),
487
     * )
488
     * </code>
489
     *
490
     * You'll note that only one command on a given table can be called.
491
     * That's a limitation of the system that's due to it being written for {@link DataObject::write()},
492
     * which needs to do a single write on a number of different tables.
493
     *
494
     * @todo Update this to support paramaterised queries
495
     *
496
     * @param array $manipulation
497
     */
498
    public static function manipulate($manipulation)
499
    {
500
        self::$lastQuery = $manipulation;
0 ignored issues
show
Documentation Bug introduced by
It seems like $manipulation of type array is incompatible with the declared type string of property $lastQuery.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
501
        self::get_conn()->manipulate($manipulation);
502
    }
503
504
    /**
505
     * Get the autogenerated ID from the previous INSERT query.
506
     *
507
     * @param string $table
508
     * @return int
509
     */
510
    public static function get_generated_id($table)
511
    {
512
        return self::get_conn()->getGeneratedID($table);
513
    }
514
515
    /**
516
     * Check if the connection to the database is active.
517
     *
518
     * @return boolean
519
     */
520
    public static function is_active()
521
    {
522
        return ($conn = self::get_conn()) && $conn->isActive();
523
    }
524
525
    /**
526
     * Create the database and connect to it. This can be called if the
527
     * initial database connection is not successful because the database
528
     * does not exist.
529
     *
530
     * @param string $database Name of database to create
531
     * @return boolean Returns true if successful
532
     */
533
    public static function create_database($database)
534
    {
535
        return self::get_conn()->selectDatabase($database, true);
536
    }
537
538
    /**
539
     * Create a new table.
540
     * @param string $table The name of the table
541
     * @param array $fields A map of field names to field types
542
     * @param array $indexes A map of indexes
543
     * @param array $options An map of additional options.  The available keys are as follows:
544
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine"
545
     *     for MySQL.
546
     *   - 'temporary' - If true, then a temporary table will be created
547
     * @param array $advancedOptions Advanced creation options
548
     * @return string The table name generated.  This may be different from the table name, for example with
549
     * temporary tables.
550
     */
551
    public static function create_table(
552
        $table,
553
        $fields = null,
554
        $indexes = null,
555
        $options = null,
556
        $advancedOptions = null
557
    ) {
558
        return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions);
559
    }
560
561
    /**
562
     * Create a new field on a table.
563
     * @param string $table Name of the table.
564
     * @param string $field Name of the field to add.
565
     * @param string $spec The field specification, eg 'INTEGER NOT NULL'
566
     */
567
    public static function create_field($table, $field, $spec)
568
    {
569
        return self::get_schema()->createField($table, $field, $spec);
570
    }
571
572
    /**
573
     * Generate the following table in the database, modifying whatever already exists
574
     * as necessary.
575
     *
576
     * @param string $table The name of the table
577
     * @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db
578
     * @param string $indexSchema A list of indexes to create.  The keys of the array are the names of the index.
579
     * The values of the array can be one of:
580
     *   - true: Create a single column index on the field named the same as the index.
581
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
582
     *     control over the index.
583
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
584
     * @param string $options SQL statement to append to the CREATE TABLE call.
585
     * @param array $extensions List of extensions
586
     */
587
    public static function require_table(
588
        $table,
589
        $fieldSchema = null,
590
        $indexSchema = null,
591
        $hasAutoIncPK = true,
592
        $options = null,
593
        $extensions = null
594
    ) {
595
        self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $options can also be of type string; however, parameter $options of SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

595
        self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, /** @scrutinizer ignore-type */ $options, $extensions);
Loading history...
Bug introduced by
It seems like $indexSchema can also be of type string; however, parameter $indexSchema of SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

595
        self::get_schema()->requireTable($table, $fieldSchema, /** @scrutinizer ignore-type */ $indexSchema, $hasAutoIncPK, $options, $extensions);
Loading history...
Bug introduced by
It seems like $fieldSchema can also be of type string; however, parameter $fieldSchema of SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

595
        self::get_schema()->requireTable($table, /** @scrutinizer ignore-type */ $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
Loading history...
596
    }
597
598
    /**
599
     * Generate the given field on the table, modifying whatever already exists as necessary.
600
     *
601
     * @param string $table The table name.
602
     * @param string $field The field name.
603
     * @param string $spec The field specification.
604
     */
605
    public static function require_field($table, $field, $spec)
606
    {
607
        self::get_schema()->requireField($table, $field, $spec);
608
    }
609
610
    /**
611
     * Generate the given index in the database, modifying whatever already exists as necessary.
612
     *
613
     * @param string $table The table name.
614
     * @param string $index The index name.
615
     * @param string|boolean $spec The specification of the index. See requireTable() for more information.
616
     */
617
    public static function require_index($table, $index, $spec)
618
    {
619
        self::get_schema()->requireIndex($table, $index, $spec);
620
    }
621
622
    /**
623
     * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
624
     *
625
     * @param string $table The table name.
626
     */
627
    public static function dont_require_table($table)
628
    {
629
        self::get_schema()->dontRequireTable($table);
630
    }
631
632
    /**
633
     * See {@link SS_Database->dontRequireField()}.
634
     *
635
     * @param string $table The table name.
636
     * @param string $fieldName The field name not to require
637
     */
638
    public static function dont_require_field($table, $fieldName)
639
    {
640
        self::get_schema()->dontRequireField($table, $fieldName);
641
    }
642
643
    /**
644
     * Checks a table's integrity and repairs it if necessary.
645
     *
646
     * @param string $table The name of the table.
647
     * @return boolean Return true if the table has integrity after the method is complete.
648
     */
649
    public static function check_and_repair_table($table)
650
    {
651
        return self::get_schema()->checkAndRepairTable($table);
652
    }
653
654
    /**
655
     * Return the number of rows affected by the previous operation.
656
     *
657
     * @return integer The number of affected rows
658
     */
659
    public static function affected_rows()
660
    {
661
        return self::get_conn()->affectedRows();
662
    }
663
664
    /**
665
     * Returns a list of all tables in the database.
666
     * The table names will be in lower case.
667
     *
668
     * @return array The list of tables
669
     */
670
    public static function table_list()
671
    {
672
        return self::get_schema()->tableList();
673
    }
674
675
    /**
676
     * Get a list of all the fields for the given table.
677
     * Returns a map of field name => field spec.
678
     *
679
     * @param string $table The table name.
680
     * @return array The list of fields
681
     */
682
    public static function field_list($table)
683
    {
684
        return self::get_schema()->fieldList($table);
685
    }
686
687
    /**
688
     * Enable suppression of database messages.
689
     *
690
     * @param bool $quiet
691
     */
692
    public static function quiet($quiet = true)
693
    {
694
        self::get_schema()->quiet($quiet);
695
    }
696
697
    /**
698
     * Show a message about database alteration
699
     *
700
     * @param string $message to display
701
     * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
702
     */
703
    public static function alteration_message($message, $type = "")
704
    {
705
        self::get_schema()->alterationMessage($message, $type);
706
    }
707
708
    /**
709
     * Check if all tables and field columns for a class exist in the database.
710
     *
711
     * @param string $class
712
     * @return boolean
713
     */
714
    public static function databaseIsReady(string $class): bool
715
    {
716
        if (!is_subclass_of($class, DataObject::class)) {
717
            throw new InvalidArgumentException("$class is not a subclass of " . DataObject::class);
718
        }
719
720
        // Don't check again if we already know the db is ready for this class.
721
        // Necessary here before the loop to catch situations where a subclass
722
        // is forced as ready without having to check all the superclasses.
723
        if (!empty(self::$databaseReadyClasses[$class])) {
724
            return true;
725
        }
726
727
        // Check if all tables and fields required for the class exist in the database.
728
        $requiredClasses = ClassInfo::dataClassesFor($class);
729
        $schema = DataObject::getSchema();
730
        foreach ($requiredClasses as $required) {
731
            // Skip test classes, as not all test classes are scaffolded at once
732
            if (is_a($required, TestOnly::class, true)) {
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\TestOnly was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
733
                continue;
734
            }
735
736
            // Don't check again if we already know the db is ready for this class.
737
            if (!empty(self::$databaseReadyClasses[$class])) {
738
                continue;
739
            }
740
741
            // if any of the tables aren't created in the database
742
            $table = $schema->tableName($required);
743
            if (!ClassInfo::hasTable($table)) {
744
                return false;
745
            }
746
747
            // HACK: DataExtensions aren't applied until a class is instantiated for
748
            // the first time, so create an instance here.
749
            singleton($required);
750
751
            // if any of the tables don't have all fields mapped as table columns
752
            $dbFields = DB::field_list($table);
753
            if (!$dbFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dbFields 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...
754
                return false;
755
            }
756
757
            $objFields = $schema->databaseFields($required, false);
758
            $missingFields = array_diff_key($objFields, $dbFields);
759
760
            if ($missingFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $missingFields 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...
761
                return false;
762
            }
763
764
            // Add each ready class to the cached array.
765
            self::$databaseReadyClasses[$required] = true;
766
        }
767
768
        return true;
769
    }
770
771
    /**
772
     * Resets the databaseReadyClasses cache.
773
     *
774
     * @param string|null $class The specific class to be cleared.
775
     * If not passed, the cache for all classes is cleared.
776
     * @param bool $clearFullHeirarchy Whether to clear the full class hierarchy or only the given class.
777
     */
778
    public static function clearDatabaseIsReady(?string $class = null, bool $clearFullHierarchy = true)
779
    {
780
        if ($class) {
781
            $clearClasses = [$class];
782
            if ($clearFullHierarchy) {
783
                $clearClasses = ClassInfo::dataClassesFor($class);
784
            }
785
            foreach ($clearClasses as $clear) {
786
                unset(self::$databaseReadyClasses[$clear]);
787
            }
788
        } else {
789
            self::$databaseReadyClasses = [];
790
        }
791
    }
792
793
    /**
794
     * For the databaseIsReady call to return a certain value for the given class - used for testing
795
     *
796
     * @param string $class The class to be forced as ready/not ready.
797
     * @param boolean $isReady The value to force.
798
     */
799
    public static function forceDatabaseIsReady(string $class, bool $isReady)
800
    {
801
        self::$databaseReadyClasses[$class] = $isReady;
802
    }
803
}
804