Passed
Pull Request — 4 (#10041)
by Guy
13:30
created

DB::affected_rows()   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 0
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
    private static $database_is_ready = [];
72
73
    /**
74
     * If set to a value for a given class, {@link database_is_ready()}
75
     * will always return that value. Used for unit testing.
76
     *
77
     * @var boolean[]
78
     */
79
    private static $force_database_is_ready = [];
80
81
    /**
82
     * The last SQL query run.
83
     * @var string
84
     */
85
    public static $lastQuery;
86
87
    /**
88
     * Internal flag to keep track of when db connection was attempted.
89
     */
90
    private static $connection_attempted = false;
91
92
    /**
93
     * Set the global database connection.
94
     * Pass an object that's a subclass of SS_Database.  This object will be used when {@link DB::query()}
95
     * is called.
96
     *
97
     * @param Database $connection The connecton object to set as the connection.
98
     * @param string $name The name to give to this connection.  If you omit this argument, the connection
99
     * will be the default one used by the ORM.  However, you can store other named connections to
100
     * be accessed through DB::get_conn($name).  This is useful when you have an application that
101
     * needs to connect to more than one database.
102
     */
103
    public static function set_conn(Database $connection, $name = 'default')
104
    {
105
        self::$connections[$name] = $connection;
106
    }
107
108
    /**
109
     * Get the global database connection.
110
     *
111
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
112
     * the default connection is returned.
113
     * @return Database
114
     */
115
    public static function get_conn($name = 'default')
116
    {
117
        if (isset(self::$connections[$name])) {
118
            return self::$connections[$name];
119
        }
120
121
        // lazy connect
122
        $config = static::getConfig($name);
123
        if ($config) {
124
            return static::connect($config, $name);
125
        }
126
127
        return null;
128
    }
129
130
    /**
131
     * @deprecated since version 4.0 Use DB::get_conn instead
132
     * @todo PSR-2 standardisation will probably un-deprecate this
133
     */
134
    public static function getConn($name = 'default')
135
    {
136
        Deprecation::notice('4.0', 'Use DB::get_conn instead');
137
        return self::get_conn($name);
138
    }
139
140
    /**
141
     * Retrieves the schema manager for the current database
142
     *
143
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
144
     * the default connection is returned.
145
     * @return DBSchemaManager
146
     */
147
    public static function get_schema($name = 'default')
148
    {
149
        $connection = self::get_conn($name);
150
        if ($connection) {
0 ignored issues
show
introduced by
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
151
            return $connection->getSchemaManager();
152
        }
153
        return null;
154
    }
155
156
    /**
157
     * Builds a sql query with the specified connection
158
     *
159
     * @param SQLExpression $expression The expression object to build from
160
     * @param array $parameters Out parameter for the resulting query parameters
161
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
162
     * the default connection is returned.
163
     * @return string The resulting SQL as a string
164
     */
165
    public static function build_sql(SQLExpression $expression, &$parameters, $name = 'default')
166
    {
167
        $connection = self::get_conn($name);
168
        if ($connection) {
0 ignored issues
show
introduced by
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
169
            return $connection->getQueryBuilder()->buildSQL($expression, $parameters);
170
        } else {
171
            $parameters = [];
172
            return null;
173
        }
174
    }
175
176
    /**
177
     * Retrieves the connector object for the current database
178
     *
179
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
180
     * the default connection is returned.
181
     * @return DBConnector
182
     */
183
    public static function get_connector($name = 'default')
184
    {
185
        $connection = self::get_conn($name);
186
        if ($connection) {
0 ignored issues
show
introduced by
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
187
            return $connection->getConnector();
188
        }
189
        return null;
190
    }
191
192
    /**
193
     * Set an alternative database in a browser cookie,
194
     * with the cookie lifetime set to the browser session.
195
     * This is useful for integration testing on temporary databases.
196
     *
197
     * There is a strict naming convention for temporary databases to avoid abuse:
198
     * <prefix> (default: 'ss_') + tmpdb + <7 digits>
199
     * As an additional security measure, temporary databases will
200
     * be ignored in "live" mode.
201
     *
202
     * Note that the database will be set on the next request.
203
     * Set it to null to revert to the main database.
204
     *
205
     * @param string $name
206
     */
207
    public static function set_alternative_database_name($name = null)
208
    {
209
        // Ignore if disabled
210
        if (!Config::inst()->get(static::class, 'alternative_database_enabled')) {
211
            return;
212
        }
213
        // Skip if CLI
214
        if (Director::is_cli()) {
215
            return;
216
        }
217
        // Validate name
218
        if ($name && !self::valid_alternative_database_name($name)) {
219
            throw new InvalidArgumentException(sprintf(
220
                'Invalid alternative database name: "%s"',
221
                $name
222
            ));
223
        }
224
225
        // Set against session
226
        if (!Injector::inst()->has(HTTPRequest::class)) {
227
            return;
228
        }
229
        /** @var HTTPRequest $request */
230
        $request = Injector::inst()->get(HTTPRequest::class);
231
        if ($name) {
232
            $request->getSession()->set(self::ALT_DB_KEY, $name);
233
        } else {
234
            $request->getSession()->clear(self::ALT_DB_KEY);
235
        }
236
    }
237
238
    /**
239
     * Get the name of the database in use
240
     *
241
     * @return string|false Name of temp database, or false if not set
242
     */
243
    public static function get_alternative_database_name()
244
    {
245
        // Ignore if disabled
246
        if (!Config::inst()->get(static::class, 'alternative_database_enabled')) {
247
            return false;
248
        }
249
        // Skip if CLI
250
        if (Director::is_cli()) {
251
            return false;
252
        }
253
        // Skip if there's no request object yet
254
        if (!Injector::inst()->has(HTTPRequest::class)) {
255
            return null;
256
        }
257
        /** @var HTTPRequest $request */
258
        $request = Injector::inst()->get(HTTPRequest::class);
259
        // Skip if the session hasn't been started
260
        if (!$request->getSession()->isStarted()) {
261
            return null;
262
        }
263
264
        $name = $request->getSession()->get(self::ALT_DB_KEY);
265
        if (self::valid_alternative_database_name($name)) {
266
            return $name;
267
        }
268
269
        return false;
270
    }
271
272
    /**
273
     * Determines if the name is valid, as a security
274
     * measure against setting arbitrary databases.
275
     *
276
     * @param string $name
277
     * @return bool
278
     */
279
    public static function valid_alternative_database_name($name)
280
    {
281
        if (Director::isLive() || empty($name)) {
282
            return false;
283
        }
284
285
        $prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
286
        $pattern = strtolower(sprintf('/^%stmpdb\d{7}$/', $prefix));
287
        return (bool)preg_match($pattern, $name);
288
    }
289
290
    /**
291
     * Specify connection to a database
292
     *
293
     * Given the database configuration, this method will create the correct
294
     * subclass of {@link SS_Database}.
295
     *
296
     * @param array $databaseConfig A map of options. The 'type' is the name of the
297
     * subclass of SS_Database to use. For the rest of the options, see the specific class.
298
     * @param string $label identifier for the connection
299
     * @return Database
300
     */
301
    public static function connect($databaseConfig, $label = 'default')
302
    {
303
        // This is used by the "testsession" module to test up a test session using an alternative name
304
        if ($name = self::get_alternative_database_name()) {
305
            $databaseConfig['database'] = $name;
306
        }
307
308
        if (!isset($databaseConfig['type']) || empty($databaseConfig['type'])) {
309
            throw new InvalidArgumentException("DB::connect: Not passed a valid database config");
310
        }
311
312
        self::$connection_attempted = true;
313
314
        $dbClass = $databaseConfig['type'];
315
316
        // Using Injector->create allows us to use registered configurations
317
        // which may or may not map to explicit objects
318
        $conn = Injector::inst()->create($dbClass);
319
        self::set_conn($conn, $label);
320
        $conn->connect($databaseConfig);
321
322
        return $conn;
323
    }
324
325
    /**
326
     * Set config for a lazy-connected database
327
     *
328
     * @param array $databaseConfig
329
     * @param string $name
330
     */
331
    public static function setConfig($databaseConfig, $name = 'default')
332
    {
333
        static::$configs[$name] = $databaseConfig;
334
    }
335
336
    /**
337
     * Get the named connection config
338
     *
339
     * @param string $name
340
     * @return mixed
341
     */
342
    public static function getConfig($name = 'default')
343
    {
344
        if (isset(static::$configs[$name])) {
345
            return static::$configs[$name];
346
        }
347
    }
348
349
    /**
350
     * Returns true if a database connection has been attempted.
351
     * In particular, it lets the caller know if we're still so early in the execution pipeline that
352
     * we haven't even tried to connect to the database yet.
353
     */
354
    public static function connection_attempted()
355
    {
356
        return self::$connection_attempted;
357
    }
358
359
    /**
360
     * Execute the given SQL query.
361
     * @param string $sql The SQL query to execute
362
     * @param int $errorLevel The level of error reporting to enable for the query
363
     * @return Query
364
     */
365
    public static function query($sql, $errorLevel = E_USER_ERROR)
366
    {
367
        self::$lastQuery = $sql;
368
369
        return self::get_conn()->query($sql, $errorLevel);
370
    }
371
372
    /**
373
     * Helper function for generating a list of parameter placeholders for the
374
     * given argument(s)
375
     *
376
     * @param array|integer $input An array of items needing placeholders, or a
377
     * number to specify the number of placeholders
378
     * @param string $join The string to join each placeholder together with
379
     * @return string|null Either a list of placeholders, or null
380
     */
381
    public static function placeholders($input, $join = ', ')
382
    {
383
        if (is_array($input)) {
384
            $number = count($input);
385
        } elseif (is_numeric($input)) {
0 ignored issues
show
introduced by
The condition is_numeric($input) is always true.
Loading history...
386
            $number = intval($input);
387
        } else {
388
            return null;
389
        }
390
        if ($number === 0) {
391
            return null;
392
        }
393
        return implode($join, array_fill(0, $number, '?'));
394
    }
395
396
    /**
397
     * @param string $sql The parameterised query
398
     * @param array $parameters The parameters to inject into the query
399
     *
400
     * @return string
401
     */
402
    public static function inline_parameters($sql, $parameters)
403
    {
404
        $segments = preg_split('/\?/', $sql);
405
        $joined = '';
406
        $inString = false;
407
        $numSegments = count($segments);
408
        for ($i = 0; $i < $numSegments; $i++) {
409
            $input = $segments[$i];
410
            // Append next segment
411
            $joined .= $segments[$i];
412
            // Don't add placeholder after last segment
413
            if ($i === $numSegments - 1) {
414
                break;
415
            }
416
            // check string escape on previous fragment
417
            // Remove escaped backslashes, count them!
418
            $input = preg_replace('/\\\\\\\\/', '', $input);
419
            // Count quotes
420
            $totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes
421
            $escapedQuotes = substr_count($input, "\\'");
422
            if ((($totalQuotes - $escapedQuotes) % 2) !== 0) {
423
                $inString = !$inString;
0 ignored issues
show
introduced by
The condition $inString is always false.
Loading history...
424
            }
425
            // Append placeholder replacement
426
            if ($inString) {
427
                // Literal question mark
428
                $joined .= '?';
429
                continue;
430
            }
431
432
            // Encode and insert next parameter
433
            $next = array_shift($parameters);
434
            if (is_array($next) && isset($next['value'])) {
435
                $next = $next['value'];
436
            }
437
            if (is_bool($next)) {
438
                $value = $next ? '1' : '0';
439
            } elseif (is_int($next)) {
440
                $value = $next;
441
            } else {
442
                $value = (DB::get_conn() !== null) ? Convert::raw2sql($next, true) : $next;
443
            }
444
            $joined .= $value;
445
        }
446
        return $joined;
447
    }
448
449
    /**
450
     * Execute the given SQL parameterised query with the specified arguments
451
     *
452
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
453
     * @param array $parameters An ordered list of arguments.
454
     * @param int $errorLevel The level of error reporting to enable for the query
455
     * @return Query
456
     */
457
    public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR)
458
    {
459
        self::$lastQuery = $sql;
460
461
        return self::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
462
    }
463
464
    /**
465
     * Execute a complex manipulation on the database.
466
     * A manipulation is an array of insert / or update sequences.  The keys of the array are table names,
467
     * and the values are map containing 'command' and 'fields'.  Command should be 'insert' or 'update',
468
     * and fields should be a map of field names to field values, including quotes.  The field value can
469
     * also be a SQL function or similar.
470
     *
471
     * Example:
472
     * <code>
473
     * array(
474
     *   // Command: insert
475
     *   "table name" => array(
476
     *      "command" => "insert",
477
     *      "fields" => array(
478
     *         "ClassName" => "'MyClass'", // if you're setting a literal, you need to escape and provide quotes
479
     *         "Created" => "now()", // alternatively, you can call DB functions
480
     *         "ID" => 234,
481
     *       ),
482
     *      "id" => 234 // an alternative to providing ID in the fields list
483
     *    ),
484
     *
485
     *   // Command: update
486
     *   "other table" => array(
487
     *      "command" => "update",
488
     *      "fields" => array(
489
     *         "ClassName" => "'MyClass'",
490
     *         "LastEdited" => "now()",
491
     *       ),
492
     *      "where" => "ID = 234",
493
     *      "id" => 234 // an alternative to providing a where clause
494
     *    ),
495
     * )
496
     * </code>
497
     *
498
     * You'll note that only one command on a given table can be called.
499
     * That's a limitation of the system that's due to it being written for {@link DataObject::write()},
500
     * which needs to do a single write on a number of different tables.
501
     *
502
     * @todo Update this to support paramaterised queries
503
     *
504
     * @param array $manipulation
505
     */
506
    public static function manipulate($manipulation)
507
    {
508
        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...
509
        self::get_conn()->manipulate($manipulation);
510
    }
511
512
    /**
513
     * Get the autogenerated ID from the previous INSERT query.
514
     *
515
     * @param string $table
516
     * @return int
517
     */
518
    public static function get_generated_id($table)
519
    {
520
        return self::get_conn()->getGeneratedID($table);
521
    }
522
523
    /**
524
     * Check if the connection to the database is active.
525
     *
526
     * @return boolean
527
     */
528
    public static function is_active()
529
    {
530
        return ($conn = self::get_conn()) && $conn->isActive();
531
    }
532
533
    /**
534
     * Create the database and connect to it. This can be called if the
535
     * initial database connection is not successful because the database
536
     * does not exist.
537
     *
538
     * @param string $database Name of database to create
539
     * @return boolean Returns true if successful
540
     */
541
    public static function create_database($database)
542
    {
543
        return self::get_conn()->selectDatabase($database, true);
544
    }
545
546
    /**
547
     * Create a new table.
548
     * @param string $table The name of the table
549
     * @param array $fields A map of field names to field types
550
     * @param array $indexes A map of indexes
551
     * @param array $options An map of additional options.  The available keys are as follows:
552
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine"
553
     *     for MySQL.
554
     *   - 'temporary' - If true, then a temporary table will be created
555
     * @param array $advancedOptions Advanced creation options
556
     * @return string The table name generated.  This may be different from the table name, for example with
557
     * temporary tables.
558
     */
559
    public static function create_table(
560
        $table,
561
        $fields = null,
562
        $indexes = null,
563
        $options = null,
564
        $advancedOptions = null
565
    ) {
566
        return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions);
567
    }
568
569
    /**
570
     * Create a new field on a table.
571
     * @param string $table Name of the table.
572
     * @param string $field Name of the field to add.
573
     * @param string $spec The field specification, eg 'INTEGER NOT NULL'
574
     */
575
    public static function create_field($table, $field, $spec)
576
    {
577
        return self::get_schema()->createField($table, $field, $spec);
578
    }
579
580
    /**
581
     * Generate the following table in the database, modifying whatever already exists
582
     * as necessary.
583
     *
584
     * @param string $table The name of the table
585
     * @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db
586
     * @param string $indexSchema A list of indexes to create.  The keys of the array are the names of the index.
587
     * The values of the array can be one of:
588
     *   - true: Create a single column index on the field named the same as the index.
589
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
590
     *     control over the index.
591
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
592
     * @param string $options SQL statement to append to the CREATE TABLE call.
593
     * @param array $extensions List of extensions
594
     */
595
    public static function require_table(
596
        $table,
597
        $fieldSchema = null,
598
        $indexSchema = null,
599
        $hasAutoIncPK = true,
600
        $options = null,
601
        $extensions = null
602
    ) {
603
        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

603
        self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, /** @scrutinizer ignore-type */ $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

603
        self::get_schema()->requireTable($table, /** @scrutinizer ignore-type */ $fieldSchema, $indexSchema, $hasAutoIncPK, $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

603
        self::get_schema()->requireTable($table, $fieldSchema, /** @scrutinizer ignore-type */ $indexSchema, $hasAutoIncPK, $options, $extensions);
Loading history...
604
    }
605
606
    /**
607
     * Generate the given field on the table, modifying whatever already exists as necessary.
608
     *
609
     * @param string $table The table name.
610
     * @param string $field The field name.
611
     * @param string $spec The field specification.
612
     */
613
    public static function require_field($table, $field, $spec)
614
    {
615
        self::get_schema()->requireField($table, $field, $spec);
616
    }
617
618
    /**
619
     * Generate the given index in the database, modifying whatever already exists as necessary.
620
     *
621
     * @param string $table The table name.
622
     * @param string $index The index name.
623
     * @param string|boolean $spec The specification of the index. See requireTable() for more information.
624
     */
625
    public static function require_index($table, $index, $spec)
626
    {
627
        self::get_schema()->requireIndex($table, $index, $spec);
628
    }
629
630
    /**
631
     * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
632
     *
633
     * @param string $table The table name.
634
     */
635
    public static function dont_require_table($table)
636
    {
637
        self::get_schema()->dontRequireTable($table);
638
    }
639
640
    /**
641
     * See {@link SS_Database->dontRequireField()}.
642
     *
643
     * @param string $table The table name.
644
     * @param string $fieldName The field name not to require
645
     */
646
    public static function dont_require_field($table, $fieldName)
647
    {
648
        self::get_schema()->dontRequireField($table, $fieldName);
649
    }
650
651
    /**
652
     * Checks a table's integrity and repairs it if necessary.
653
     *
654
     * @param string $table The name of the table.
655
     * @return boolean Return true if the table has integrity after the method is complete.
656
     */
657
    public static function check_and_repair_table($table)
658
    {
659
        return self::get_schema()->checkAndRepairTable($table);
660
    }
661
662
    /**
663
     * Return the number of rows affected by the previous operation.
664
     *
665
     * @return integer The number of affected rows
666
     */
667
    public static function affected_rows()
668
    {
669
        return self::get_conn()->affectedRows();
670
    }
671
672
    /**
673
     * Returns a list of all tables in the database.
674
     * The table names will be in lower case.
675
     *
676
     * @return array The list of tables
677
     */
678
    public static function table_list()
679
    {
680
        return self::get_schema()->tableList();
681
    }
682
683
    /**
684
     * Get a list of all the fields for the given table.
685
     * Returns a map of field name => field spec.
686
     *
687
     * @param string $table The table name.
688
     * @return array The list of fields
689
     */
690
    public static function field_list($table)
691
    {
692
        return self::get_schema()->fieldList($table);
693
    }
694
695
    /**
696
     * Enable supression of database messages.
697
     *
698
     * @param bool $quiet
699
     */
700
    public static function quiet($quiet = true)
701
    {
702
        self::get_schema()->quiet($quiet);
703
    }
704
705
    /**
706
     * Show a message about database alteration
707
     *
708
     * @param string $message to display
709
     * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
710
     */
711
    public static function alteration_message($message, $type = "")
712
    {
713
        self::get_schema()->alterationMessage($message, $type);
714
    }
715
716
    /**
717
     * Check if all tables and field columns for a class exist in the database.
718
     *
719
     * @param string $class
720
     * @return boolean
721
     */
722
    public static function database_is_ready(string $class): bool
723
    {
724
        if (!is_subclass_of($class, DataObject::class)) {
725
            throw new InvalidArgumentException("$class is not a subclass of " . DataObject::class);
726
        }
727
728
        // Used for unit tests
729
        if (array_key_exists($class, self::$force_database_is_ready)) {
730
            return self::$force_database_is_ready[$class];
731
        }
732
733
        // Don't check again if we already know the db is ready for this class.
734
        if (!empty(self::$database_is_ready[$class])) {
735
            return true;
736
        }
737
738
        // Check if all tables and fields required for the class exist in the database.
739
        $requiredClasses = ClassInfo::dataClassesFor($class);
740
        $schema = DataObject::getSchema();
741
        foreach ($requiredClasses as $required) {
742
            // Skip test classes, as not all test classes are scaffolded at once
743
            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...
744
                continue;
745
            }
746
747
            // if any of the tables aren't created in the database
748
            $table = $schema->tableName($required);
749
            if (!ClassInfo::hasTable($table)) {
750
                return false;
751
            }
752
753
            // HACK: DataExtensions aren't applied until a class is instantiated for
754
            // the first time, so create an instance here.
755
            singleton($required);
756
757
            // if any of the tables don't have all fields mapped as table columns
758
            $dbFields = DB::field_list($table);
759
            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...
760
                return false;
761
            }
762
763
            $objFields = $schema->databaseFields($required, false);
764
            $missingFields = array_diff_key($objFields, $dbFields);
765
766
            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...
767
                return false;
768
            }
769
        }
770
        self::$database_is_ready[$class] = true;
771
772
        return true;
773
    }
774
775
    /**
776
     * Resets the database_is_ready cache.
777
     *
778
     * @param string $class The specific class to be cleared.
779
     * If not passed, the cache for all classes is cleared.
780
     */
781
    public static function clear_database_is_ready(string $class)
782
    {
783
        if ($class) {
784
            unset(self::$database_is_ready[$class]);
785
            unset(self::$force_database_is_ready[$class]);
786
        } else {
787
            self::$database_is_ready = [];
788
            self::$force_database_is_ready = [];
789
        }
790
    }
791
792
    /**
793
     * For the database_is_ready call to return a certain value for the given class - used for testing
794
     *
795
     * @param string $class The class to be forced as ready/not ready.
796
     * @param boolean $isReady The value to force.
797
     */
798
    public static function force_database_is_ready(string $class, bool $isReady)
799
    {
800
        self::$force_database_is_ready[$class] = $isReady;
801
    }
802
}
803