Passed
Push — int-types ( 949579...4b2be5 )
by Sam
05:58
created

MySQLDatabase::searchEngine()   F

Complexity

Conditions 12
Paths 386

Size

Total Lines 142
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 83
nc 386
nop 9
dl 0
loc 142
rs 3.4175
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use SilverStripe\Assets\File;
6
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTree 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...
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\ORM\PaginatedList;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\ArrayList;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\Queries\SQLSelect;
14
use Exception;
15
16
/**
17
 * MySQL connector class.
18
 *
19
 * Supported indexes for {@link requireTable()}
20
 *
21
 * You are advised to backup your tables if changing settings on an existing database
22
 * `connection_charset` and `charset` should be equal, similarly so should `connection_collation` and `collation`
23
 */
24
class MySQLDatabase extends Database implements TransactionManager
25
{
26
    use Configurable;
27
28
    /**
29
     * Default connection charset (may be overridden in $databaseConfig)
30
     *
31
     * @config
32
     * @var String
33
     */
34
    private static $connection_charset = 'utf8';
0 ignored issues
show
introduced by
The private property $connection_charset is not used, and could be removed.
Loading history...
35
36
    /**
37
     * Default connection collation
38
     *
39
     * @config
40
     * @var string
41
     */
42
    private static $connection_collation = 'utf8_general_ci';
0 ignored issues
show
introduced by
The private property $connection_collation is not used, and could be removed.
Loading history...
43
44
    /**
45
     * Default charset
46
     *
47
     * @config
48
     * @var string
49
     */
50
    private static $charset = 'utf8';
0 ignored issues
show
introduced by
The private property $charset is not used, and could be removed.
Loading history...
51
52
    /**
53
     * Cache for getTransactionManager()
54
     * @var TransactionManager
55
     */
56
    private $transactionManager = null;
57
58
    /**
59
     * Default collation
60
     *
61
     * @config
62
     * @var string
63
     */
64
    private static $collation = 'utf8_general_ci';
0 ignored issues
show
introduced by
The private property $collation is not used, and could be removed.
Loading history...
65
66
    public function connect($parameters)
67
    {
68
        // Ensure that driver is available (required by PDO)
69
        if (empty($parameters['driver'])) {
70
            $parameters['driver'] = $this->getDatabaseServer();
71
        }
72
73
        // Set charset
74
        if (empty($parameters['charset']) && ($charset = static::config()->get('connection_charset'))) {
75
            $parameters['charset'] = $charset;
76
        }
77
78
        // Set collation
79
        if (empty($parameters['collation']) && ($collation = static::config()->get('connection_collation'))) {
80
            $parameters['collation'] = $collation;
81
        }
82
83
        // Notify connector of parameters
84
        $this->connector->connect($parameters);
85
86
        // This is important!
87
        $this->setSQLMode('ANSI');
88
89
        if (isset($parameters['timezone'])) {
90
            $this->selectTimezone($parameters['timezone']);
91
        }
92
93
        // SS_Database subclass maintains responsibility for selecting database
94
        // once connected in order to correctly handle schema queries about
95
        // existence of database, error handling at the correct level, etc
96
        if (!empty($parameters['database'])) {
97
            $this->selectDatabase($parameters['database'], false, false);
98
        }
99
    }
100
101
    /**
102
     * Sets the SQL mode
103
     *
104
     * @param string $mode Connection mode
105
     */
106
    public function setSQLMode($mode)
107
    {
108
        if (empty($mode)) {
109
            return;
110
        }
111
        $this->preparedQuery("SET sql_mode = ?", array($mode));
112
    }
113
114
    /**
115
     * Sets the system timezone for the database connection
116
     *
117
     * @param string $timezone
118
     */
119
    public function selectTimezone($timezone)
120
    {
121
        if (empty($timezone)) {
122
            return;
123
        }
124
        $this->preparedQuery("SET SESSION time_zone = ?", array($timezone));
125
    }
126
127
    public function supportsCollations()
128
    {
129
        return true;
130
    }
131
132
    public function supportsTimezoneOverride()
133
    {
134
        return true;
135
    }
136
137
    public function getDatabaseServer()
138
    {
139
        return "mysql";
140
    }
141
142
    /**
143
     * The core search engine, used by this class and its subclasses to do fun stuff.
144
     * Searches both SiteTree and File.
145
     *
146
     * @param array $classesToSearch
147
     * @param string $keywords Keywords as a string.
148
     * @param int $start
149
     * @param int $pageLength
150
     * @param string $sortBy
151
     * @param string $extraFilter
152
     * @param bool $booleanSearch
153
     * @param string $alternativeFileFilter
154
     * @param bool $invertedMatch
155
     * @return PaginatedList
156
     * @throws Exception
157
     */
158
    public function searchEngine(
159
        $classesToSearch,
160
        $keywords,
161
        $start,
162
        $pageLength,
163
        $sortBy = "Relevance DESC",
164
        $extraFilter = "",
165
        $booleanSearch = false,
166
        $alternativeFileFilter = "",
167
        $invertedMatch = false
168
    ) {
169
        $pageClass = SiteTree::class;
170
        $fileClass = File::class;
171
        if (!class_exists($pageClass)) {
172
            throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class');
173
        }
174
        if (!class_exists($fileClass)) {
175
            throw new Exception('MySQLDatabase->searchEngine() requires "File" class');
176
        }
177
178
        $keywords = $this->escapeString($keywords);
179
        $htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8');
180
181
        $extraFilters = array($pageClass => '', $fileClass => '');
182
183
        $boolean = '';
184
        if ($booleanSearch) {
185
            $boolean = "IN BOOLEAN MODE";
186
        }
187
188
        if ($extraFilter) {
189
            $extraFilters[$pageClass] = " AND $extraFilter";
190
191
            if ($alternativeFileFilter) {
192
                $extraFilters[$fileClass] = " AND $alternativeFileFilter";
193
            } else {
194
                $extraFilters[$fileClass] = $extraFilters[$pageClass];
195
            }
196
        }
197
198
        // Always ensure that only pages with ShowInSearch = 1 can be searched
199
        $extraFilters[$pageClass] .= " AND ShowInSearch <> 0";
200
201
        // File.ShowInSearch was added later, keep the database driver backwards compatible
202
        // by checking for its existence first
203
        $fileTable = DataObject::getSchema()->tableName($fileClass);
204
        $fields = $this->getSchemaManager()->fieldList($fileTable);
205
        if (array_key_exists('ShowInSearch', $fields)) {
206
            $extraFilters[$fileClass] .= " AND ShowInSearch <> 0";
207
        }
208
209
        $limit = (int)$start . ", " . (int)$pageLength;
210
211
        $notMatch = $invertedMatch
212
                ? "NOT "
213
                : "";
214
        if ($keywords) {
215
            $match[$pageClass] = "
0 ignored issues
show
Comprehensibility Best Practice introduced by
$match was never initialized. Although not strictly required by PHP, it is generally a good practice to add $match = array(); before regardless.
Loading history...
216
				MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$keywords' $boolean)
217
				+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityKeywords' $boolean)
218
			";
219
            $fileClassSQL = Convert::raw2sql($fileClass);
220
            $match[$fileClass] = "MATCH (Name, Title) AGAINST ('$keywords' $boolean) AND ClassName = '$fileClassSQL'";
221
222
            // We make the relevance search by converting a boolean mode search into a normal one
223
            $relevanceKeywords = str_replace(array('*', '+', '-'), '', $keywords);
224
            $htmlEntityRelevanceKeywords = str_replace(array('*', '+', '-'), '', $htmlEntityKeywords);
225
            $relevance[$pageClass] = "MATCH (Title, MenuTitle, Content, MetaDescription) "
0 ignored issues
show
Comprehensibility Best Practice introduced by
$relevance was never initialized. Although not strictly required by PHP, it is generally a good practice to add $relevance = array(); before regardless.
Loading history...
226
                    . "AGAINST ('$relevanceKeywords') "
227
                    . "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityRelevanceKeywords')";
228
            $relevance[$fileClass] = "MATCH (Name, Title) AGAINST ('$relevanceKeywords')";
229
        } else {
230
            $relevance[$pageClass] = $relevance[$fileClass] = 1;
231
            $match[$pageClass] = $match[$fileClass] = "1 = 1";
232
        }
233
234
        // Generate initial DataLists and base table names
235
        $lists = array();
236
        $sqlTables = array($pageClass => '', $fileClass => '');
237
        foreach ($classesToSearch as $class) {
238
            $lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class]);
239
            $sqlTables[$class] = '"' . DataObject::getSchema()->tableName($class) . '"';
240
        }
241
242
        $charset = static::config()->get('charset');
243
244
        // Make column selection lists
245
        $select = array(
246
            $pageClass => array(
247
                "ClassName", "{$sqlTables[$pageClass]}.\"ID\"", "ParentID",
248
                "Title", "MenuTitle", "URLSegment", "Content",
249
                "LastEdited", "Created",
250
                "Name" => "_{$charset}''",
251
                "Relevance" => $relevance[$pageClass], "CanViewType"
252
            ),
253
            $fileClass => array(
254
                "ClassName", "{$sqlTables[$fileClass]}.\"ID\"", "ParentID",
255
                "Title", "MenuTitle" => "_{$charset}''", "URLSegment" => "_{$charset}''", "Content" => "_{$charset}''",
256
                "LastEdited", "Created",
257
                "Name",
258
                "Relevance" => $relevance[$fileClass], "CanViewType" => "NULL"
259
            ),
260
        );
261
262
        // Process and combine queries
263
        $querySQLs = array();
264
        $queryParameters = array();
265
        $totalCount = 0;
266
        foreach ($lists as $class => $list) {
267
            /** @var SQLSelect $query */
268
            $query = $list->dataQuery()->query();
269
270
            // There's no need to do all that joining
271
            $query->setFrom($sqlTables[$class]);
272
            $query->setSelect($select[$class]);
273
            $query->setOrderBy(array());
274
275
            $querySQLs[] = $query->sql($parameters);
276
            $queryParameters = array_merge($queryParameters, $parameters);
277
278
            $totalCount += $query->unlimitedRowCount();
279
        }
280
        $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
281
282
        // Get records
283
        $records = $this->preparedQuery($fullQuery, $queryParameters);
284
285
        $objects = array();
286
287
        foreach ($records as $record) {
288
            $objects[] = new $record['ClassName']($record);
289
        }
290
291
        $list = new PaginatedList(new ArrayList($objects));
292
        $list->setPageStart($start);
293
        $list->setPageLength($pageLength);
294
        $list->setTotalItems($totalCount);
295
296
        // The list has already been limited by the query above
297
        $list->setLimitItems(false);
298
299
        return $list;
300
    }
301
302
303
    /**
304
     * Returns the TransactionManager to handle transactions for this database.
305
     *
306
     * @return TransactionManager
307
     */
308
    protected function getTransactionManager()
309
    {
310
        if (!$this->transactionManager) {
311
            // PDOConnector providers this
312
            if ($this->connector instanceof TransactionManager) {
313
                $this->transactionManager = new NestedTransactionManager($this->connector);
314
            // Direct database access does not
315
            } else {
316
                $this->transactionManager = new NestedTransactionManager(new MySQLTransactionManager($this));
317
            }
318
        }
319
        return $this->transactionManager;
320
    }
321
    public function supportsTransactions()
322
    {
323
        return true;
324
    }
325
326
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
327
    {
328
        $this->getTransactionManager()->transactionStart($transactionMode, $sessionCharacteristics);
329
    }
330
331
    public function transactionSavepoint($savepoint)
332
    {
333
        user_error('transactionSavepoint() no longer does anything', E_USER_WARNING);
334
    }
335
336
    public function transactionRollback($savepoint = false)
337
    {
338
        if ($savepoint) {
339
            user_error('transactionRollback() $savepont option no longer does anything', E_USER_WARNING);
340
        }
341
342
        return $this->getTransactionManager()->transactionRollback();
343
    }
344
345
    public function transactionDepth()
346
    {
347
        return $this->getTransactionManager()->transactionDepth();
348
    }
349
350
    public function transactionEnd($chain = false)
351
    {
352
        $result = $this->getTransactionManager()->transactionEnd();
353
354
        if ($chain) {
355
            Deprecation::notice('4.3', '$chain argument is dereprecated');
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\Connect\Deprecation 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...
356
            return $this->getTransactionManager()->transactionStart();
357
        }
358
359
        return $result;
360
    }
361
362
    /**
363
     * In error condition, set transactionNesting to zero
364
     */
365
    protected function resetTransactionNesting()
366
    {
367
        // Check whether to use a connector's built-in transaction methods
368
        if ($this->connector instanceof TransactionalDBConnector) {
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\Connect\TransactionalDBConnector 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...
369
            if ($this->transactionNesting > 0) {
370
                $this->connector->transactionRollback();
0 ignored issues
show
Bug introduced by
The method transactionRollback() does not exist on SilverStripe\ORM\Connect\DBConnector. It seems like you code against a sub-type of SilverStripe\ORM\Connect\DBConnector such as SilverStripe\ORM\Connect\PDOConnector. ( Ignorable by Annotation )

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

370
                $this->connector->/** @scrutinizer ignore-call */ 
371
                                  transactionRollback();
Loading history...
371
            }
372
        }
373
        $this->transactionNesting = 0;
0 ignored issues
show
Bug Best Practice introduced by
The property transactionNesting does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
374
    }
375
376
    public function query($sql, $errorLevel = E_USER_ERROR)
377
    {
378
        $this->inspectQuery($sql);
379
        return parent::query($sql, $errorLevel);
380
    }
381
382
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
383
    {
384
        $this->inspectQuery($sql);
385
        return parent::preparedQuery($sql, $parameters, $errorLevel);
386
    }
387
388
    /**
389
     * Inspect a SQL query prior to execution
390
     *
391
     * @param string $sql
392
     */
393
    protected function inspectQuery($sql)
394
    {
395
        // Any DDL discards transactions.
396
        // See https://dev.mysql.com/doc/internals/en/transactions-notes-on-ddl-and-normal-transaction.html
397
        // on why we need to be over-eager
398
        $isDDL = $this->getConnector()->isQueryDDL($sql);
399
        if ($isDDL) {
400
            $this->resetTransactionNesting();
401
        }
402
    }
403
404
    public function comparisonClause(
405
        $field,
406
        $value,
407
        $exact = false,
408
        $negate = false,
409
        $caseSensitive = null,
410
        $parameterised = false
411
    ) {
412
        if ($exact && $caseSensitive === null) {
413
            $comp = ($negate) ? '!=' : '=';
414
        } else {
415
            $comp = ($caseSensitive) ? 'LIKE BINARY' : 'LIKE';
416
            if ($negate) {
417
                $comp = 'NOT ' . $comp;
418
            }
419
        }
420
421
        if ($parameterised) {
422
            return sprintf("%s %s ?", $field, $comp);
423
        } else {
424
            return sprintf("%s %s '%s'", $field, $comp, $value);
425
        }
426
    }
427
428
    public function formattedDatetimeClause($date, $format)
429
    {
430
        preg_match_all('/%(.)/', $format, $matches);
431
        foreach ($matches[1] as $match) {
432
            if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
433
                user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
434
            }
435
        }
436
437
        if (preg_match('/^now$/i', $date)) {
438
            $date = "NOW()";
439
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
440
            $date = "'$date'";
441
        }
442
443
        if ($format == '%U') {
444
            return "UNIX_TIMESTAMP($date)";
445
        }
446
447
        return "DATE_FORMAT($date, '$format')";
448
    }
449
450
    public function datetimeIntervalClause($date, $interval)
451
    {
452
        $interval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
453
454
        if (preg_match('/^now$/i', $date)) {
455
            $date = "NOW()";
456
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
457
            $date = "'$date'";
458
        }
459
460
        return "$date + INTERVAL $interval";
461
    }
462
463
    public function datetimeDifferenceClause($date1, $date2)
464
    {
465
        // First date format
466
        if (preg_match('/^now$/i', $date1)) {
467
            $date1 = "NOW()";
468
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
469
            $date1 = "'$date1'";
470
        }
471
        // Second date format
472
        if (preg_match('/^now$/i', $date2)) {
473
            $date2 = "NOW()";
474
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
475
            $date2 = "'$date2'";
476
        }
477
478
        return "UNIX_TIMESTAMP($date1) - UNIX_TIMESTAMP($date2)";
479
    }
480
481
    public function supportsLocks()
482
    {
483
        return true;
484
    }
485
486
    public function canLock($name)
487
    {
488
        $id = $this->getLockIdentifier($name);
489
        return (bool) $this->query(sprintf("SELECT IS_FREE_LOCK('%s')", $id))->value();
490
    }
491
492
    public function getLock($name, $timeout = 5)
493
    {
494
        $id = $this->getLockIdentifier($name);
495
496
        // MySQL auto-releases existing locks on subsequent GET_LOCK() calls,
497
        // in contrast to PostgreSQL and SQL Server who stack the locks.
498
        return (bool) $this->query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value();
499
    }
500
501
    public function releaseLock($name)
502
    {
503
        $id = $this->getLockIdentifier($name);
504
        return (bool) $this->query(sprintf("SELECT RELEASE_LOCK('%s')", $id))->value();
505
    }
506
507
    protected function getLockIdentifier($name)
508
    {
509
        // Prefix with database name
510
        $dbName = $this->connector->getSelectedDatabase() ;
511
        return $this->escapeString("{$dbName}_{$name}");
512
    }
513
514
    public function now()
515
    {
516
        // MySQL uses NOW() to return the current date/time.
517
        return 'NOW()';
518
    }
519
520
    public function random()
521
    {
522
        return 'RAND()';
523
    }
524
525
    /**
526
     * Clear all data in a given table
527
     *
528
     * @param string $table Name of table
529
     */
530
    public function clearTable($table)
531
    {
532
        $this->query("DELETE FROM \"$table\"");
533
534
        // Check if resetting the auto-increment is needed
535
        $autoIncrement = $this->preparedQuery(
536
            'SELECT "AUTO_INCREMENT" FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?',
537
            [ $this->getSelectedDatabase(), $table]
538
        )->value();
539
540
        if ($autoIncrement > 1) {
541
            $this->query("ALTER TABLE \"$table\" AUTO_INCREMENT = 1");
542
        }
543
    }
544
}
545