Issues (2882)

src/ORM/DB.php (11 issues)

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\Config\Config;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Environment;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Dev\Deprecation;
14
use SilverStripe\ORM\Connect\Database;
15
use SilverStripe\ORM\Connect\DBConnector;
16
use SilverStripe\ORM\Connect\DBSchemaManager;
17
use SilverStripe\ORM\Connect\Query;
18
use SilverStripe\ORM\Queries\SQLExpression;
19
20
/**
21
 * Global database interface, complete with static methods.
22
 * Use this class for interacting with the database.
23
 */
24
class DB
25
{
26
27
    /**
28
     * This constant was added in SilverStripe 2.4 to indicate that SQL-queries
29
     * should now use ANSI-compatible syntax.  The most notable affect of this
30
     * change is that table and field names should be escaped with double quotes
31
     * and not backticks
32
     */
33
    const USE_ANSI_SQL = true;
34
35
    /**
36
     * Session key for alternative database name
37
     */
38
    const ALT_DB_KEY = 'alternativeDatabaseName';
39
40
    /**
41
     * Allow alternative DB to be disabled.
42
     * Necessary for DB backed session store to work.
43
     *
44
     * @config
45
     * @var bool
46
     */
47
    private static $alternative_database_enabled = true;
0 ignored issues
show
The private property $alternative_database_enabled is not used, and could be removed.
Loading history...
48
49
    /**
50
     * The global database connection.
51
     *
52
     * @var Database
53
     */
54
    protected static $connections = [];
55
56
    /**
57
     * List of configurations for each connection
58
     *
59
     * @var array List of configs each in the $databaseConfig format
60
     */
61
    protected static $configs = [];
62
63
64
65
    /**
66
     * The last SQL query run.
67
     * @var string
68
     */
69
    public static $lastQuery;
70
71
    /**
72
     * Internal flag to keep track of when db connection was attempted.
73
     */
74
    private static $connection_attempted = false;
75
76
    /**
77
     * Set the global database connection.
78
     * Pass an object that's a subclass of SS_Database.  This object will be used when {@link DB::query()}
79
     * is called.
80
     *
81
     * @param Database $connection The connecton object to set as the connection.
82
     * @param string $name The name to give to this connection.  If you omit this argument, the connection
83
     * will be the default one used by the ORM.  However, you can store other named connections to
84
     * be accessed through DB::get_conn($name).  This is useful when you have an application that
85
     * needs to connect to more than one database.
86
     */
87
    public static function set_conn(Database $connection, $name = 'default')
88
    {
89
        self::$connections[$name] = $connection;
90
    }
91
92
    /**
93
     * Get the global database connection.
94
     *
95
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
96
     * the default connection is returned.
97
     * @return Database
98
     */
99
    public static function get_conn($name = 'default')
100
    {
101
        if (isset(self::$connections[$name])) {
102
            return self::$connections[$name];
103
        }
104
105
        // lazy connect
106
        $config = static::getConfig($name);
107
        if ($config) {
108
            return static::connect($config, $name);
109
        }
110
111
        return null;
112
    }
113
114
    /**
115
     * @deprecated since version 4.0 Use DB::get_conn instead
116
     * @todo PSR-2 standardisation will probably un-deprecate this
117
     */
118
    public static function getConn($name = 'default')
119
    {
120
        Deprecation::notice('4.0', 'Use DB::get_conn instead');
121
        return self::get_conn($name);
122
    }
123
124
    /**
125
     * Retrieves the schema manager for the current database
126
     *
127
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
128
     * the default connection is returned.
129
     * @return DBSchemaManager
130
     */
131
    public static function get_schema($name = 'default')
132
    {
133
        $connection = self::get_conn($name);
134
        if ($connection) {
0 ignored issues
show
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
135
            return $connection->getSchemaManager();
136
        }
137
        return null;
138
    }
139
140
    /**
141
     * Builds a sql query with the specified connection
142
     *
143
     * @param SQLExpression $expression The expression object to build from
144
     * @param array $parameters Out parameter for the resulting query parameters
145
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
146
     * the default connection is returned.
147
     * @return string The resulting SQL as a string
148
     */
149
    public static function build_sql(SQLExpression $expression, &$parameters, $name = 'default')
150
    {
151
        $connection = self::get_conn($name);
152
        if ($connection) {
0 ignored issues
show
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
153
            return $connection->getQueryBuilder()->buildSQL($expression, $parameters);
154
        } else {
155
            $parameters = array();
156
            return null;
157
        }
158
    }
159
160
    /**
161
     * Retrieves the connector object for the current database
162
     *
163
     * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
164
     * the default connection is returned.
165
     * @return DBConnector
166
     */
167
    public static function get_connector($name = 'default')
168
    {
169
        $connection = self::get_conn($name);
170
        if ($connection) {
0 ignored issues
show
$connection is of type SilverStripe\ORM\Connect\Database, thus it always evaluated to true.
Loading history...
171
            return $connection->getConnector();
172
        }
173
        return null;
174
    }
175
176
    /**
177
     * Set an alternative database in a browser cookie,
178
     * with the cookie lifetime set to the browser session.
179
     * This is useful for integration testing on temporary databases.
180
     *
181
     * There is a strict naming convention for temporary databases to avoid abuse:
182
     * <prefix> (default: 'ss_') + tmpdb + <7 digits>
183
     * As an additional security measure, temporary databases will
184
     * be ignored in "live" mode.
185
     *
186
     * Note that the database will be set on the next request.
187
     * Set it to null to revert to the main database.
188
     *
189
     * @param string $name
190
     */
191
    public static function set_alternative_database_name($name = null)
192
    {
193
        // Ignore if disabled
194
        if (!Config::inst()->get(static::class, 'alternative_database_enabled')) {
195
            return;
196
        }
197
        // Skip if CLI
198
        if (Director::is_cli()) {
199
            return;
200
        }
201
        // Validate name
202
        if ($name && !self::valid_alternative_database_name($name)) {
203
            throw new InvalidArgumentException(sprintf(
204
                'Invalid alternative database name: "%s"',
205
                $name
206
            ));
207
        }
208
209
        // Set against session
210
        if (!Injector::inst()->has(HTTPRequest::class)) {
211
            return;
212
        }
213
        /** @var HTTPRequest $request */
214
        $request = Injector::inst()->get(HTTPRequest::class);
215
        if ($name) {
216
            $request->getSession()->set(self::ALT_DB_KEY, $name);
217
        } else {
218
            $request->getSession()->clear(self::ALT_DB_KEY);
219
        }
220
    }
221
222
    /**
223
     * Get the name of the database in use
224
     *
225
     * @return string|false Name of temp database, or false if not set
226
     */
227
    public static function get_alternative_database_name()
228
    {
229
        // Ignore if disabled
230
        if (!Config::inst()->get(static::class, 'alternative_database_enabled')) {
231
            return false;
232
        }
233
        // Skip if CLI
234
        if (Director::is_cli()) {
235
            return false;
236
        }
237
        // Skip if there's no request object yet
238
        if (!Injector::inst()->has(HTTPRequest::class)) {
239
            return null;
240
        }
241
        /** @var HTTPRequest $request */
242
        $request = Injector::inst()->get(HTTPRequest::class);
243
        // Skip if the session hasn't been started
244
        if (!$request->getSession()->isStarted()) {
245
            return null;
246
        }
247
248
        $name = $request->getSession()->get(self::ALT_DB_KEY);
249
        if (self::valid_alternative_database_name($name)) {
250
            return $name;
251
        }
252
253
        return false;
254
    }
255
256
    /**
257
     * Determines if the name is valid, as a security
258
     * measure against setting arbitrary databases.
259
     *
260
     * @param string $name
261
     * @return bool
262
     */
263
    public static function valid_alternative_database_name($name)
264
    {
265
        if (Director::isLive() || empty($name)) {
266
            return false;
267
        }
268
269
        $prefix = Environment::getEnv('SS_DATABASE_PREFIX') ?: 'ss_';
270
        $pattern = strtolower(sprintf('/^%stmpdb\d{7}$/', $prefix));
271
        return (bool)preg_match($pattern, $name);
272
    }
273
274
    /**
275
     * Specify connection to a database
276
     *
277
     * Given the database configuration, this method will create the correct
278
     * subclass of {@link SS_Database}.
279
     *
280
     * @param array $databaseConfig A map of options. The 'type' is the name of the
281
     * subclass of SS_Database to use. For the rest of the options, see the specific class.
282
     * @param string $label identifier for the connection
283
     * @return Database
284
     */
285
    public static function connect($databaseConfig, $label = 'default')
286
    {
287
        // This is used by the "testsession" module to test up a test session using an alternative name
288
        if ($name = self::get_alternative_database_name()) {
289
            $databaseConfig['database'] = $name;
290
        }
291
292
        if (!isset($databaseConfig['type']) || empty($databaseConfig['type'])) {
293
            throw new InvalidArgumentException("DB::connect: Not passed a valid database config");
294
        }
295
296
        self::$connection_attempted = true;
297
298
        $dbClass = $databaseConfig['type'];
299
300
        // Using Injector->create allows us to use registered configurations
301
        // which may or may not map to explicit objects
302
        $conn = Injector::inst()->create($dbClass);
303
        self::set_conn($conn, $label);
304
        $conn->connect($databaseConfig);
305
306
        return $conn;
307
    }
308
309
    /**
310
     * Set config for a lazy-connected database
311
     *
312
     * @param array $databaseConfig
313
     * @param string $name
314
     */
315
    public static function setConfig($databaseConfig, $name = 'default')
316
    {
317
        static::$configs[$name] = $databaseConfig;
318
    }
319
320
    /**
321
     * Get the named connection config
322
     *
323
     * @param string $name
324
     * @return mixed
325
     */
326
    public static function getConfig($name = 'default')
327
    {
328
        if (isset(static::$configs[$name])) {
329
            return static::$configs[$name];
330
        }
331
    }
332
333
    /**
334
     * Returns true if a database connection has been attempted.
335
     * In particular, it lets the caller know if we're still so early in the execution pipeline that
336
     * we haven't even tried to connect to the database yet.
337
     */
338
    public static function connection_attempted()
339
    {
340
        return self::$connection_attempted;
341
    }
342
343
    /**
344
     * Execute the given SQL query.
345
     * @param string $sql The SQL query to execute
346
     * @param int $errorLevel The level of error reporting to enable for the query
347
     * @return Query
348
     */
349
    public static function query($sql, $errorLevel = E_USER_ERROR)
350
    {
351
        self::$lastQuery = $sql;
352
353
        return self::get_conn()->query($sql, $errorLevel);
354
    }
355
356
    /**
357
     * Helper function for generating a list of parameter placeholders for the
358
     * given argument(s)
359
     *
360
     * @param array|integer $input An array of items needing placeholders, or a
361
     * number to specify the number of placeholders
362
     * @param string $join The string to join each placeholder together with
363
     * @return string|null Either a list of placeholders, or null
364
     */
365
    public static function placeholders($input, $join = ', ')
366
    {
367
        if (is_array($input)) {
368
            $number = count($input);
369
        } elseif (is_numeric($input)) {
0 ignored issues
show
The condition is_numeric($input) is always true.
Loading history...
370
            $number = intval($input);
371
        } else {
372
            return null;
373
        }
374
        if ($number === 0) {
375
            return null;
376
        }
377
        return implode($join, array_fill(0, $number, '?'));
378
    }
379
380
    /**
381
     * @param string $sql The parameterised query
382
     * @param array $parameters The parameters to inject into the query
383
     *
384
     * @return string
385
     */
386
    public static function inline_parameters($sql, $parameters)
387
    {
388
        $segments = preg_split('/\?/', $sql);
389
        $joined = '';
390
        $inString = false;
391
        $numSegments = count($segments);
0 ignored issues
show
It seems like $segments can also be of type false; however, parameter $var of count() does only seem to accept Countable|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

391
        $numSegments = count(/** @scrutinizer ignore-type */ $segments);
Loading history...
392
        for ($i = 0; $i < $numSegments; $i++) {
393
            $input = $segments[$i];
394
            // Append next segment
395
            $joined .= $segments[$i];
396
            // Don't add placeholder after last segment
397
            if ($i === $numSegments - 1) {
398
                break;
399
            }
400
            // check string escape on previous fragment
401
            // Remove escaped backslashes, count them!
402
            $input = preg_replace('/\\\\\\\\/', '', $input);
403
            // Count quotes
404
            $totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes
405
            $escapedQuotes = substr_count($input, "\\'");
406
            if ((($totalQuotes - $escapedQuotes) % 2) !== 0) {
407
                $inString = !$inString;
0 ignored issues
show
The condition $inString is always false.
Loading history...
408
            }
409
            // Append placeholder replacement
410
            if ($inString) {
411
                // Literal question mark
412
                $joined .= '?';
413
                continue;
414
            }
415
416
            // Encode and insert next parameter
417
            $next = array_shift($parameters);
418
            if (is_array($next) && isset($next['value'])) {
419
                $next = $next['value'];
420
            }
421
            if (is_bool($next)) {
422
                $value = $next ? '1' : '0';
423
            } elseif (is_int($next)) {
424
                $value = $next;
425
            } else {
426
                $value = (DB::get_conn() !== null) ? Convert::raw2sql($next, true) : $next;
427
            }
428
            $joined .= $value;
429
        }
430
        return $joined;
431
    }
432
433
    /**
434
     * Execute the given SQL parameterised query with the specified arguments
435
     *
436
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
437
     * @param array $parameters An ordered list of arguments.
438
     * @param int $errorLevel The level of error reporting to enable for the query
439
     * @return Query
440
     */
441
    public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR)
442
    {
443
        self::$lastQuery = $sql;
444
445
        return self::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
446
    }
447
448
    /**
449
     * Execute a complex manipulation on the database.
450
     * A manipulation is an array of insert / or update sequences.  The keys of the array are table names,
451
     * and the values are map containing 'command' and 'fields'.  Command should be 'insert' or 'update',
452
     * and fields should be a map of field names to field values, including quotes.  The field value can
453
     * also be a SQL function or similar.
454
     *
455
     * Example:
456
     * <code>
457
     * array(
458
     *   // Command: insert
459
     *   "table name" => array(
460
     *      "command" => "insert",
461
     *      "fields" => array(
462
     *         "ClassName" => "'MyClass'", // if you're setting a literal, you need to escape and provide quotes
463
     *         "Created" => "now()", // alternatively, you can call DB functions
464
     *         "ID" => 234,
465
     *       ),
466
     *      "id" => 234 // an alternative to providing ID in the fields list
467
     *    ),
468
     *
469
     *   // Command: update
470
     *   "other table" => array(
471
     *      "command" => "update",
472
     *      "fields" => array(
473
     *         "ClassName" => "'MyClass'",
474
     *         "LastEdited" => "now()",
475
     *       ),
476
     *      "where" => "ID = 234",
477
     *      "id" => 234 // an alternative to providing a where clause
478
     *    ),
479
     * )
480
     * </code>
481
     *
482
     * You'll note that only one command on a given table can be called.
483
     * That's a limitation of the system that's due to it being written for {@link DataObject::write()},
484
     * which needs to do a single write on a number of different tables.
485
     *
486
     * @todo Update this to support paramaterised queries
487
     *
488
     * @param array $manipulation
489
     */
490
    public static function manipulate($manipulation)
491
    {
492
        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...
493
        self::get_conn()->manipulate($manipulation);
494
    }
495
496
    /**
497
     * Get the autogenerated ID from the previous INSERT query.
498
     *
499
     * @param string $table
500
     * @return int
501
     */
502
    public static function get_generated_id($table)
503
    {
504
        return self::get_conn()->getGeneratedID($table);
505
    }
506
507
    /**
508
     * Check if the connection to the database is active.
509
     *
510
     * @return boolean
511
     */
512
    public static function is_active()
513
    {
514
        return ($conn = self::get_conn()) && $conn->isActive();
515
    }
516
517
    /**
518
     * Create the database and connect to it. This can be called if the
519
     * initial database connection is not successful because the database
520
     * does not exist.
521
     *
522
     * @param string $database Name of database to create
523
     * @return boolean Returns true if successful
524
     */
525
    public static function create_database($database)
526
    {
527
        return self::get_conn()->selectDatabase($database, true);
528
    }
529
530
    /**
531
     * Create a new table.
532
     * @param string $table The name of the table
533
     * @param array$fields A map of field names to field types
534
     * @param array $indexes A map of indexes
535
     * @param array $options An map of additional options.  The available keys are as follows:
536
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine"
537
     *     for MySQL.
538
     *   - 'temporary' - If true, then a temporary table will be created
539
     * @param array $advancedOptions Advanced creation options
540
     * @return string The table name generated.  This may be different from the table name, for example with
541
     * temporary tables.
542
     */
543
    public static function create_table(
544
        $table,
545
        $fields = null,
546
        $indexes = null,
547
        $options = null,
548
        $advancedOptions = null
549
    ) {
550
        return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions);
551
    }
552
553
    /**
554
     * Create a new field on a table.
555
     * @param string $table Name of the table.
556
     * @param string $field Name of the field to add.
557
     * @param string $spec The field specification, eg 'INTEGER NOT NULL'
558
     */
559
    public static function create_field($table, $field, $spec)
560
    {
561
        return self::get_schema()->createField($table, $field, $spec);
562
    }
563
564
    /**
565
     * Generate the following table in the database, modifying whatever already exists
566
     * as necessary.
567
     *
568
     * @param string $table The name of the table
569
     * @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db
570
     * @param string $indexSchema A list of indexes to create.  The keys of the array are the names of the index.
571
     * The values of the array can be one of:
572
     *   - true: Create a single column index on the field named the same as the index.
573
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
574
     *     control over the index.
575
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
576
     * @param string $options SQL statement to append to the CREATE TABLE call.
577
     * @param array $extensions List of extensions
578
     */
579
    public static function require_table(
580
        $table,
581
        $fieldSchema = null,
582
        $indexSchema = null,
583
        $hasAutoIncPK = true,
584
        $options = null,
585
        $extensions = null
586
    ) {
587
        self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
0 ignored issues
show
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

587
        self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, /** @scrutinizer ignore-type */ $options, $extensions);
Loading history...
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

587
        self::get_schema()->requireTable($table, /** @scrutinizer ignore-type */ $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
Loading history...
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

587
        self::get_schema()->requireTable($table, $fieldSchema, /** @scrutinizer ignore-type */ $indexSchema, $hasAutoIncPK, $options, $extensions);
Loading history...
588
    }
589
590
    /**
591
     * Generate the given field on the table, modifying whatever already exists as necessary.
592
     *
593
     * @param string $table The table name.
594
     * @param string $field The field name.
595
     * @param string $spec The field specification.
596
     */
597
    public static function require_field($table, $field, $spec)
598
    {
599
        self::get_schema()->requireField($table, $field, $spec);
600
    }
601
602
    /**
603
     * Generate the given index in the database, modifying whatever already exists as necessary.
604
     *
605
     * @param string $table The table name.
606
     * @param string $index The index name.
607
     * @param string|boolean $spec The specification of the index. See requireTable() for more information.
608
     */
609
    public static function require_index($table, $index, $spec)
610
    {
611
        self::get_schema()->requireIndex($table, $index, $spec);
612
    }
613
614
    /**
615
     * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
616
     *
617
     * @param string $table The table name.
618
     */
619
    public static function dont_require_table($table)
620
    {
621
        self::get_schema()->dontRequireTable($table);
622
    }
623
624
    /**
625
     * See {@link SS_Database->dontRequireField()}.
626
     *
627
     * @param string $table The table name.
628
     * @param string $fieldName The field name not to require
629
     */
630
    public static function dont_require_field($table, $fieldName)
631
    {
632
        self::get_schema()->dontRequireField($table, $fieldName);
633
    }
634
635
    /**
636
     * Checks a table's integrity and repairs it if necessary.
637
     *
638
     * @param string $table The name of the table.
639
     * @return boolean Return true if the table has integrity after the method is complete.
640
     */
641
    public static function check_and_repair_table($table)
642
    {
643
        return self::get_schema()->checkAndRepairTable($table);
644
    }
645
646
    /**
647
     * Return the number of rows affected by the previous operation.
648
     *
649
     * @return integer The number of affected rows
650
     */
651
    public static function affected_rows()
652
    {
653
        return self::get_conn()->affectedRows();
654
    }
655
656
    /**
657
     * Returns a list of all tables in the database.
658
     * The table names will be in lower case.
659
     *
660
     * @return array The list of tables
661
     */
662
    public static function table_list()
663
    {
664
        return self::get_schema()->tableList();
665
    }
666
667
    /**
668
     * Get a list of all the fields for the given table.
669
     * Returns a map of field name => field spec.
670
     *
671
     * @param string $table The table name.
672
     * @return array The list of fields
673
     */
674
    public static function field_list($table)
675
    {
676
        return self::get_schema()->fieldList($table);
677
    }
678
679
    /**
680
     * Enable supression of database messages.
681
     *
682
     * @param bool $quiet
683
     */
684
    public static function quiet($quiet = true)
685
    {
686
        self::get_schema()->quiet($quiet);
687
    }
688
689
    /**
690
     * Show a message about database alteration
691
     *
692
     * @param string $message to display
693
     * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
694
     */
695
    public static function alteration_message($message, $type = "")
696
    {
697
        self::get_schema()->alterationMessage($message, $type);
698
    }
699
}
700