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

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