Total Complexity | 184 |
Total Lines | 1176 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like DB often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DB, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
23 | class DB extends \PDO |
||
24 | { |
||
25 | /** |
||
26 | * @var bool |
||
27 | */ |
||
28 | public $cli; |
||
29 | |||
30 | /** |
||
31 | * @var mixed |
||
32 | */ |
||
33 | public $ct; |
||
34 | |||
35 | /** |
||
36 | * @var \Blacklight\ColorCLI Instance variable for logging object. Currently only ColorCLI supported, |
||
37 | * but expanding for full logging with agnostic API planned. |
||
38 | */ |
||
39 | public $log; |
||
40 | |||
41 | /** |
||
42 | * @note Setting this static causes issues when creating multiple instances of this class with different |
||
43 | * MySQL servers, the next instances re-uses the server of the first instance. |
||
44 | * @var \PDO Instance of PDO class. |
||
45 | */ |
||
46 | public $pdo = null; |
||
47 | |||
48 | /** |
||
49 | * @var bool |
||
50 | */ |
||
51 | protected $_debug; |
||
52 | |||
53 | /** |
||
54 | * @var \Blacklight\Logger |
||
|
|||
55 | */ |
||
56 | private $debugging; |
||
57 | |||
58 | /** |
||
59 | * @var string Lower-cased name of DBMS in use. |
||
60 | */ |
||
61 | private $dbSystem; |
||
62 | |||
63 | /** |
||
64 | * @var string Version of the Db server. |
||
65 | */ |
||
66 | private $dbVersion; |
||
67 | |||
68 | /** |
||
69 | * @var string Stored copy of the dsn used to connect. |
||
70 | */ |
||
71 | private $dsn; |
||
72 | |||
73 | /** |
||
74 | * @var array Options passed into the constructor or defaulted. |
||
75 | */ |
||
76 | private $opts; |
||
77 | |||
78 | /** |
||
79 | * @var \Blacklight\libraries\Cache |
||
80 | */ |
||
81 | private $cacheServer; |
||
82 | |||
83 | /** |
||
84 | * @var bool Should we cache the results of the query method? |
||
85 | */ |
||
86 | private $cacheEnabled = false; |
||
87 | |||
88 | /** |
||
89 | * @var string MySQL LOW_PRIORITY DELETE option. |
||
90 | */ |
||
91 | private $DELETE_LOW_PRIORITY = ''; |
||
92 | |||
93 | /** |
||
94 | * @var string MYSQL QUICK DELETE option. |
||
95 | */ |
||
96 | private $DELETE_QUICK = ''; |
||
97 | |||
98 | /** |
||
99 | * Constructor. Sets up all necessary properties. Instantiates a PDO object |
||
100 | * if needed, otherwise returns the current one. |
||
101 | * |
||
102 | * @param array $options |
||
103 | * @throws \Exception |
||
104 | */ |
||
105 | public function __construct(array $options = []) |
||
106 | { |
||
107 | $this->cli = Utility::isCLI(); |
||
108 | |||
109 | $defaults = [ |
||
110 | 'checkVersion' => false, |
||
111 | 'createDb' => false, // create dbname if it does not exist? |
||
112 | 'ct' => new ConsoleTools(), |
||
113 | 'dbhost' => env('DB_HOST', '127.0.0.1'), |
||
114 | 'dbname' => env('DB_NAME', 'nntmux'), |
||
115 | 'dbpass' => env('DB_PASSWORD', 'nntmux'), |
||
116 | 'dbport' => env('DB_PORT', '3306'), |
||
117 | 'dbsock' => env('DB_SOCKET', ''), |
||
118 | 'dbtype' => env('DB_SYSTEM', 'mysql'), |
||
119 | 'dbuser' => env('DB_USER', 'nntmux'), |
||
120 | 'log' => new ColorCLI(), |
||
121 | 'persist' => false, |
||
122 | ]; |
||
123 | $options += $defaults; |
||
124 | |||
125 | if (! $this->cli) { |
||
126 | $options['log'] = null; |
||
127 | } |
||
128 | $this->opts = $options; |
||
129 | |||
130 | if (! empty($this->opts['dbtype'])) { |
||
131 | $this->dbSystem = strtolower($this->opts['dbtype']); |
||
132 | } |
||
133 | |||
134 | if (! ($this->pdo instanceof \PDO)) { |
||
135 | $this->initialiseDatabase(); |
||
136 | } |
||
137 | |||
138 | $this->cacheEnabled = \defined('NN_CACHE_TYPE') && (NN_CACHE_TYPE > 0); |
||
139 | |||
140 | if ($this->cacheEnabled) { |
||
141 | try { |
||
142 | $this->cacheServer = new Cache(); |
||
143 | } catch (CacheException $error) { |
||
144 | $this->cacheEnabled = false; |
||
145 | $this->echoError($error->getMessage(), '__construct', 4); |
||
146 | } |
||
147 | } |
||
148 | |||
149 | $this->ct = $this->opts['ct']; |
||
150 | $this->log = $this->opts['log']; |
||
151 | |||
152 | if ($this->opts['checkVersion']) { |
||
153 | $this->fetchDbVersion(); |
||
154 | } |
||
155 | } |
||
156 | |||
157 | public function __destruct() |
||
158 | { |
||
159 | $this->pdo = null; |
||
160 | } |
||
161 | |||
162 | public function checkDbExists($name = null) |
||
163 | { |
||
164 | if (empty($name)) { |
||
165 | $name = $this->opts['dbname']; |
||
166 | } |
||
167 | |||
168 | $found = false; |
||
169 | $tables = $this->getTableList(); |
||
170 | foreach ($tables as $table) { |
||
171 | if ($table['Database'] === $name) { |
||
172 | $found = true; |
||
173 | break; |
||
174 | } |
||
175 | } |
||
176 | |||
177 | return $found; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Looks up info for index on table. |
||
182 | * |
||
183 | * @param $table string Table to look at. |
||
184 | * @param $index string Index to check. |
||
185 | * |
||
186 | * @return bool|array False on failure, associative array of SHOW data. |
||
187 | */ |
||
188 | public function checkIndex($table, $index) |
||
189 | { |
||
190 | $result = $this->pdo->query( |
||
191 | sprintf( |
||
192 | "SHOW INDEX FROM %s WHERE key_name = '%s'", |
||
193 | trim($table), |
||
194 | trim($index) |
||
195 | ) |
||
196 | ); |
||
197 | if ($result === false) { |
||
198 | return false; |
||
199 | } |
||
200 | |||
201 | return $result->fetch(\PDO::FETCH_ASSOC); |
||
202 | } |
||
203 | |||
204 | public function checkColumnIndex($table, $column) |
||
205 | { |
||
206 | $result = $this->pdo->query( |
||
207 | sprintf( |
||
208 | "SHOW INDEXES IN %s WHERE non_unique = 0 AND column_name = '%s'", |
||
209 | trim($table), |
||
210 | trim($column) |
||
211 | ) |
||
212 | ); |
||
213 | if ($result === false) { |
||
214 | return false; |
||
215 | } |
||
216 | |||
217 | return $result->fetchAll(\PDO::FETCH_ASSOC); |
||
218 | } |
||
219 | |||
220 | public function getTableList() |
||
221 | { |
||
222 | $result = $this->pdo->query('SHOW DATABASES'); |
||
223 | |||
224 | return $result->fetchAll(\PDO::FETCH_ASSOC); |
||
225 | } |
||
226 | |||
227 | /** |
||
228 | * Attempts to determine if the Db is on the local machine. |
||
229 | * |
||
230 | * If the method returns true, then the Db is definitely on the local machine. However, |
||
231 | * returning false only indicates that it could not positively be determined to be local - so |
||
232 | * assume remote. |
||
233 | * |
||
234 | * @return bool Whether the Db is definitely on the local machine. |
||
235 | */ |
||
236 | public function isLocalDb(): bool |
||
237 | { |
||
238 | $local = false; |
||
239 | if (! empty($this->opts['dbsock']) || $this->opts['dbhost'] === 'localhost') { |
||
240 | $local = true; |
||
241 | } else { |
||
242 | preg_match_all('/inet'.'6?'.' addr: ?([^ ]+)/', `ifconfig`, $ips); |
||
243 | |||
244 | // Check for dotted quad - if exists compare against local IP number(s) |
||
245 | if (preg_match('#^\d+\.\d+\.\d+\.\d+$#', $this->opts['dbhost'])) { |
||
246 | if (\in_array($this->opts['dbhost'], $ips[1], false)) { |
||
247 | $local = true; |
||
248 | } |
||
249 | } |
||
250 | } |
||
251 | |||
252 | return $local; |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * Init PDO instance. |
||
257 | * |
||
258 | * @throws \RuntimeException |
||
259 | */ |
||
260 | private function initialiseDatabase() |
||
261 | { |
||
262 | if (! empty($this->opts['dbsock'])) { |
||
263 | $dsn = $this->dbSystem.':unix_socket='.$this->opts['dbsock']; |
||
264 | } else { |
||
265 | $dsn = $this->dbSystem.':host='.$this->opts['dbhost']; |
||
266 | if (! empty($this->opts['dbport'])) { |
||
267 | $dsn .= ';port='.$this->opts['dbport']; |
||
268 | } |
||
269 | } |
||
270 | $dsn .= ';charset=utf8'; |
||
271 | |||
272 | $options = [ |
||
273 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, |
||
274 | \PDO::ATTR_TIMEOUT => 180, |
||
275 | \PDO::ATTR_PERSISTENT => $this->opts['persist'], |
||
276 | \PDO::MYSQL_ATTR_LOCAL_INFILE => true, |
||
277 | ]; |
||
278 | |||
279 | $this->dsn = $dsn; |
||
280 | // removed try/catch to let the instantiating code handle the problem (Install for |
||
281 | // instance can output a message that connecting failed. |
||
282 | $this->pdo = new \PDO($dsn, $this->opts['dbuser'], $this->opts['dbpass'], $options); |
||
283 | |||
284 | if ($this->opts['dbname'] !== '') { |
||
285 | if ($this->opts['createDb']) { |
||
286 | $found = $this->checkDbExists(); |
||
287 | if ($found) { |
||
288 | try { |
||
289 | $this->pdo->exec('DROP DATABASE '.$this->opts['dbname']); |
||
290 | } catch (\Exception $e) { |
||
291 | throw new \RuntimeException("Error trying to drop your old database: '{$this->opts['dbname']}'", 2); |
||
292 | } |
||
293 | $found = $this->checkDbExists(); |
||
294 | } |
||
295 | |||
296 | if ($found) { |
||
297 | throw new \RuntimeException("Could not drop your old database: '{$this->opts['dbname']}'", 2); |
||
298 | } |
||
299 | $this->pdo->exec("CREATE DATABASE `{$this->opts['dbname']}` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci"); |
||
300 | |||
301 | if (! $this->checkDbExists()) { |
||
302 | throw new \RuntimeException("Could not create new database: '{$this->opts['dbname']}'", 3); |
||
303 | } |
||
304 | } |
||
305 | $this->pdo->exec("USE {$this->opts['dbname']}"); |
||
306 | } |
||
307 | |||
308 | // In case PDO is not set to produce exceptions (PHP's default behaviour). |
||
309 | if ($this->pdo === false) { |
||
310 | $this->echoError( |
||
311 | 'Unable to create connection to the Database!', |
||
312 | 'initialiseDatabase', |
||
313 | 1, |
||
314 | true |
||
315 | ); |
||
316 | } |
||
317 | |||
318 | // For backwards compatibility, no need for a patch. |
||
319 | $this->pdo->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER); |
||
320 | $this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * Echo error, optionally exit. |
||
325 | * |
||
326 | * @param string $error The error message. |
||
327 | * @param string $method The method where the error occured. |
||
328 | * @param int $severity The severity of the error. |
||
329 | * @param bool $exit Exit or not? |
||
330 | */ |
||
331 | protected function echoError($error, $method, $severity, $exit = false) |
||
342 | } |
||
343 | } |
||
344 | |||
345 | /** |
||
346 | * @return string mysql. |
||
347 | */ |
||
348 | public function DbSystem(): string |
||
349 | { |
||
350 | return $this->dbSystem; |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Returns a string, escaped with single quotes, false on failure. http://www.php.net/manual/en/pdo.quote.php. |
||
355 | * |
||
356 | * @param string $str |
||
357 | * |
||
358 | * @return string |
||
359 | */ |
||
360 | public function escapeString($str): string |
||
361 | { |
||
362 | if ($str === null) { |
||
363 | return 'NULL'; |
||
364 | } |
||
365 | |||
366 | return $this->pdo->quote($str); |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * Formats a 'like' string. ex.(LIKE '%chocolate%'). |
||
371 | * |
||
372 | * @param string $str The string. |
||
373 | * @param bool $left Add a % to the left. |
||
374 | * @param bool $right Add a % to the right. |
||
375 | * |
||
376 | * @return string |
||
377 | */ |
||
378 | public function likeString($str, $left = true, $right = true): string |
||
379 | { |
||
380 | return 'LIKE '.$this->escapeString(($left ? '%' : '').$str.($right ? '%' : '')); |
||
381 | } |
||
382 | |||
383 | /** |
||
384 | * Verify if pdo var is instance of PDO class. |
||
385 | * |
||
386 | * @return bool |
||
387 | */ |
||
388 | public function isInitialised(): bool |
||
389 | { |
||
390 | return $this->pdo instanceof \PDO; |
||
391 | } |
||
392 | |||
393 | /** |
||
394 | * For inserting a row. Returns last insert ID. queryExec is better if you do not need the id. |
||
395 | * |
||
396 | * @param string $query |
||
397 | * |
||
398 | * @return int|false|string |
||
399 | */ |
||
400 | public function queryInsert($query) |
||
401 | { |
||
402 | if (! $this->parseQuery($query)) { |
||
403 | return false; |
||
404 | } |
||
405 | |||
406 | $i = 2; |
||
407 | $error = ''; |
||
408 | while ($i < 11) { |
||
409 | $result = $this->queryExecHelper($query, true); |
||
410 | if (\is_array($result) && isset($result['deadlock'])) { |
||
411 | $error = $result['message']; |
||
412 | if ($result['deadlock'] === true) { |
||
413 | $this->echoError( |
||
414 | 'A Deadlock or lock wait timeout has occurred, sleeping. ('. |
||
415 | ($i - 1).')', |
||
416 | 'queryInsert', |
||
417 | 4 |
||
418 | ); |
||
419 | $this->ct->showsleep($i * ($i / 2)); |
||
420 | $i++; |
||
421 | } else { |
||
422 | break; |
||
423 | } |
||
424 | } elseif ($result === false) { |
||
425 | $error = 'Unspecified error.'; |
||
426 | break; |
||
427 | } else { |
||
428 | return $result; |
||
429 | } |
||
430 | } |
||
431 | |||
432 | return false; |
||
433 | } |
||
434 | |||
435 | /** |
||
436 | * Delete rows from MySQL. |
||
437 | * |
||
438 | * @param string $query |
||
439 | * @param bool $silent Echo or log errors? |
||
440 | * |
||
441 | * @return bool|\PDOStatement |
||
442 | */ |
||
443 | public function queryDelete($query, $silent = false) |
||
444 | { |
||
445 | // Accommodate for chained queries (SELECT 1;DELETE x FROM y) |
||
446 | if (preg_match('#(.*?[^a-z0-9]|^)DELETE\s+(.+?)$#is', $query, $matches)) { |
||
447 | $query = $matches[1].'DELETE '.$this->DELETE_LOW_PRIORITY.$this->DELETE_QUICK.$matches[2]; |
||
448 | } |
||
449 | |||
450 | return $this->queryExec($query, $silent); |
||
451 | } |
||
452 | |||
453 | /** |
||
454 | * Used for deleting, updating (and inserting without needing the last insert id). |
||
455 | * |
||
456 | * @param string $query |
||
457 | * @param bool $silent Echo or log errors? |
||
458 | * |
||
459 | * @return bool|\PDOStatement |
||
460 | */ |
||
461 | public function queryExec($query, $silent = false) |
||
462 | { |
||
463 | if (! $this->parseQuery($query)) { |
||
464 | return false; |
||
465 | } |
||
466 | |||
467 | $i = 2; |
||
468 | $error = ''; |
||
469 | while ($i < 11) { |
||
470 | $result = $this->queryExecHelper($query); |
||
471 | if (\is_array($result) && isset($result['deadlock'])) { |
||
472 | $error = $result['message']; |
||
473 | if ($result['deadlock'] === true) { |
||
474 | $this->echoError('A Deadlock or lock wait timeout has occurred, sleeping. ('.($i - 1).')', 'queryExec', 4); |
||
475 | $this->ct->showsleep($i * ($i / 2)); |
||
476 | $i++; |
||
477 | } else { |
||
478 | break; |
||
479 | } |
||
480 | } elseif ($result === false) { |
||
481 | $error = 'Unspecified error.'; |
||
482 | break; |
||
483 | } else { |
||
484 | return $result; |
||
485 | } |
||
486 | } |
||
487 | |||
488 | return false; |
||
489 | } |
||
490 | |||
491 | /** |
||
492 | * Helper method for queryInsert and queryExec, checks for deadlocks. |
||
493 | * |
||
494 | * @param $query |
||
495 | * @param bool $insert |
||
496 | * @return array|\PDOStatement|string |
||
497 | * @throws \RuntimeException |
||
498 | */ |
||
499 | protected function queryExecHelper($query, $insert = false) |
||
500 | { |
||
501 | try { |
||
502 | if ($insert === false) { |
||
503 | return $this->pdo->query($query); |
||
504 | } |
||
505 | $this->pdo->exec($query); |
||
506 | |||
507 | return $this->pdo->lastInsertId(); |
||
508 | } catch (\PDOException $e) { |
||
509 | // Deadlock or lock wait timeout, try 10 times. |
||
510 | if ( |
||
511 | $e->errorInfo[1] === 1213 || |
||
512 | $e->errorInfo[0] === 40001 || |
||
513 | $e->errorInfo[1] === 1205 || |
||
514 | $e->getMessage() === 'SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction' |
||
515 | ) { |
||
516 | return ['deadlock' => true, 'message' => $e->getMessage()]; |
||
517 | } |
||
518 | |||
519 | // Check if we lost connection to MySQL. |
||
520 | if ($this->_checkGoneAway($e->getMessage()) !== false) { |
||
521 | |||
522 | // Reconnect to MySQL. |
||
523 | if ($this->_reconnect() === true) { |
||
524 | |||
525 | // If we reconnected, retry the query. |
||
526 | return $this->queryExecHelper($query, $insert); |
||
527 | } |
||
528 | } |
||
529 | |||
530 | return ['deadlock' => false, 'message' => $e->getMessage()]; |
||
531 | } |
||
532 | } |
||
533 | |||
534 | /** |
||
535 | * Direct query. Return the affected row count. http://www.php.net/manual/en/pdo.exec.php. |
||
536 | * |
||
537 | * @note If not "consumed", causes this error: |
||
538 | * 'SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active. |
||
539 | * Consider using PDOStatement::fetchAll(). Alternatively, if your code is only ever going to run against mysql, |
||
540 | * you may enable query buffering by setting the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute.' |
||
541 | * |
||
542 | * @param string $query |
||
543 | * @param bool $silent Whether to skip echoing errors to the console. |
||
544 | * |
||
545 | * @return bool|int|\PDOStatement |
||
546 | * @throws \RuntimeException |
||
547 | */ |
||
548 | public function exec($query, $silent = false) |
||
549 | { |
||
550 | if (! $this->parseQuery($query)) { |
||
551 | return false; |
||
552 | } |
||
553 | |||
554 | try { |
||
555 | return $this->pdo->exec($query); |
||
556 | } catch (\PDOException $e) { |
||
557 | |||
558 | // Check if we lost connection to MySQL. |
||
559 | if ($this->_checkGoneAway($e->getMessage()) !== false) { |
||
560 | |||
561 | // Reconnect to MySQL. |
||
562 | if ($this->_reconnect() === true) { |
||
563 | |||
564 | // If we reconnected, retry the query. |
||
565 | return $this->exec($query, $silent); |
||
566 | } else { |
||
567 | // If we are not reconnected, return false. |
||
568 | return false; |
||
569 | } |
||
570 | } elseif (! $silent) { |
||
571 | $this->echoError($e->getMessage(), 'Exec', 4, false); |
||
572 | } |
||
573 | |||
574 | return false; |
||
575 | } |
||
576 | } |
||
577 | |||
578 | /** |
||
579 | * Returns an array of result (empty array if no results or an error occurs) |
||
580 | * Optional: Pass true to cache the result with a cache server. |
||
581 | * |
||
582 | * @param string $query SQL to execute. |
||
583 | * @param bool $cache Indicates if the query result should be cached. |
||
584 | * @param int $cacheExpiry The time in seconds before deleting the query result from the cache server. |
||
585 | * |
||
586 | * @return array|bool Array of results (possibly empty) on success, empty array on failure. |
||
587 | */ |
||
588 | public function query($query, $cache = false, $cacheExpiry = 600): array |
||
589 | { |
||
590 | if (! $this->parseQuery($query)) { |
||
591 | return false; |
||
592 | } |
||
593 | |||
594 | if ($cache === true && $this->cacheEnabled === true) { |
||
595 | try { |
||
596 | $data = $this->cacheServer->get($this->cacheServer->createKey($query)); |
||
597 | if ($data !== false) { |
||
598 | return $data; |
||
599 | } |
||
600 | } catch (CacheException $error) { |
||
601 | $this->echoError($error->getMessage(), 'query', 4); |
||
602 | } |
||
603 | } |
||
604 | |||
605 | $result = $this->queryArray($query); |
||
606 | |||
607 | if ($result !== false && $cache === true && $this->cacheEnabled === true) { |
||
608 | $this->cacheServer->set($this->cacheServer->createKey($query), $result, $cacheExpiry); |
||
609 | } |
||
610 | |||
611 | return ($result === false) ? [] : $result; |
||
612 | } |
||
613 | |||
614 | /** |
||
615 | * Returns a multidimensional array of result of the query function return and the count of found rows |
||
616 | * Note: Query passed to this function SHOULD include SQL_CALC_FOUND_ROWS |
||
617 | * Optional: Pass true to cache the result with a cache server. |
||
618 | * |
||
619 | * @param string $query SQL to execute. |
||
620 | * @param bool $cache Indicates if the query result should be cached. |
||
621 | * @param int $cacheExpiry The time in seconds before deleting the query result from the cache server. |
||
622 | * |
||
623 | * @return array Array of results (possibly empty) on success, empty array on failure. |
||
624 | */ |
||
625 | public function queryCalc($query, $cache = false, $cacheExpiry = 600): array |
||
626 | { |
||
627 | $data = $this->query($query, $cache, $cacheExpiry); |
||
628 | |||
629 | if (strpos($query, 'SQL_CALC_FOUND_ROWS') === false) { |
||
630 | return $data; |
||
631 | } |
||
632 | |||
633 | // Remove LIMIT and OFFSET from query to allow queryCalc usage with browse |
||
634 | $query = preg_replace('#(\s+LIMIT\s+\d+)?\s+OFFSET\s+\d+\s*$#i', '', $query); |
||
635 | |||
636 | if ($cache === true && $this->cacheEnabled === true) { |
||
637 | try { |
||
638 | $count = $this->cacheServer->get($this->cacheServer->createKey($query.'count')); |
||
639 | if ($count !== false) { |
||
640 | return ['total' => $count, 'result' => $data]; |
||
641 | } |
||
642 | } catch (CacheException $error) { |
||
643 | $this->echoError($error->getMessage(), 'queryCalc', 4); |
||
644 | } |
||
645 | } |
||
646 | |||
647 | $result = $this->queryOneRow('SELECT FOUND_ROWS() AS total'); |
||
648 | |||
649 | if ($result !== false && $cache === true && $this->cacheEnabled === true) { |
||
650 | $this->cacheServer->set($this->cacheServer->createKey($query.'count'), $result['total'], $cacheExpiry); |
||
651 | } |
||
652 | |||
653 | return |
||
654 | [ |
||
655 | 'total' => $result === false ? 0 : $result['total'], |
||
656 | 'result' => $data, |
||
657 | ]; |
||
658 | } |
||
659 | |||
660 | /** |
||
661 | * Main method for creating results as an array. |
||
662 | * |
||
663 | * @param string $query SQL to execute. |
||
664 | * |
||
665 | * @return array|bool Array of results on success or false on failure. |
||
666 | */ |
||
667 | public function queryArray($query) |
||
668 | { |
||
669 | $result = false; |
||
670 | if (! empty($query)) { |
||
671 | $result = $this->queryDirect($query); |
||
672 | |||
673 | if (! empty($result)) { |
||
674 | $result = $result->fetchAll(); |
||
675 | } |
||
676 | } |
||
677 | |||
678 | return $result; |
||
679 | } |
||
680 | |||
681 | /** |
||
682 | * Returns all results as an associative array. |
||
683 | * |
||
684 | * Do not use this function for large dat-asets, as it can cripple the Db server and use huge |
||
685 | * amounts of RAM. Instead iterate through the data. |
||
686 | * |
||
687 | * @param string $query The query to execute. |
||
688 | * |
||
689 | * @return array|bool Array of results on success, false otherwise. |
||
690 | */ |
||
691 | public function queryAssoc($query) |
||
692 | { |
||
693 | if ($query === '') { |
||
694 | return false; |
||
695 | } |
||
696 | $mode = $this->pdo->getAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE); |
||
697 | if ($mode !== \PDO::FETCH_ASSOC) { |
||
698 | $this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); |
||
699 | } |
||
700 | |||
701 | $result = $this->queryArray($query); |
||
702 | |||
703 | if ($mode !== \PDO::FETCH_ASSOC) { |
||
704 | // Restore old mode |
||
705 | $this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, $mode); |
||
706 | } |
||
707 | |||
708 | return $result; |
||
709 | } |
||
710 | |||
711 | /** |
||
712 | * Query without returning an empty array like our function query(). http://php.net/manual/en/pdo.query.php. |
||
713 | * |
||
714 | * @param string $query The query to run. |
||
715 | * @param bool $ignore Ignore errors, do not log them? |
||
716 | * |
||
717 | * @return bool|\PDOStatement |
||
718 | * @throws \RuntimeException |
||
719 | */ |
||
720 | public function queryDirect($query, $ignore = false) |
||
721 | { |
||
722 | if (! $this->parseQuery($query)) { |
||
723 | return false; |
||
724 | } |
||
725 | |||
726 | try { |
||
727 | $result = $this->pdo->query($query); |
||
728 | } catch (\PDOException $e) { |
||
729 | |||
730 | // Check if we lost connection to MySQL. |
||
731 | if ($this->_checkGoneAway($e->getMessage()) !== false) { |
||
732 | |||
733 | // Reconnect to MySQL. |
||
734 | if ($this->_reconnect() === true) { |
||
735 | |||
736 | // If we reconnected, retry the query. |
||
737 | $result = $this->queryDirect($query); |
||
738 | } else { |
||
739 | // If we are not reconnected, return false. |
||
740 | $result = false; |
||
741 | } |
||
742 | } else { |
||
743 | if ($ignore === false) { |
||
744 | $this->echoError($e->getMessage(), 'queryDirect', 4, false); |
||
745 | } |
||
746 | $result = false; |
||
747 | } |
||
748 | } |
||
749 | |||
750 | return $result; |
||
751 | } |
||
752 | |||
753 | /** |
||
754 | * Reconnect to MySQL when the connection has been lost. |
||
755 | * |
||
756 | * @see ping(), _checkGoneAway() for checking the connection. |
||
757 | * |
||
758 | * @return bool |
||
759 | * @throws \RuntimeException |
||
760 | */ |
||
761 | protected function _reconnect(): bool |
||
762 | { |
||
763 | $this->initialiseDatabase(); |
||
764 | |||
765 | // Check if we are really connected to MySQL. |
||
766 | return ! ($this->ping() === false); |
||
767 | } |
||
768 | |||
769 | /** |
||
770 | * Verify that we've lost a connection to MySQL. |
||
771 | * |
||
772 | * @param string $errorMessage |
||
773 | * |
||
774 | * @return bool |
||
775 | */ |
||
776 | protected function _checkGoneAway($errorMessage): bool |
||
777 | { |
||
778 | return stripos($errorMessage, 'MySQL server has gone away') !== false; |
||
779 | } |
||
780 | |||
781 | /** |
||
782 | * Returns the first row of the query. |
||
783 | * |
||
784 | * @param string $query |
||
785 | * @param bool $appendLimit |
||
786 | * |
||
787 | * @return array|bool |
||
788 | */ |
||
789 | public function queryOneRow($query, $appendLimit = true) |
||
790 | { |
||
791 | // Force the query to only return 1 row, so queryArray doesn't potentially run out of memory on a large data set. |
||
792 | // First check if query already contains a LIMIT clause. |
||
793 | if (preg_match('#\s+LIMIT\s+(?P<lower>\d+)(,\s+(?P<upper>\d+))?(;)?$#i', $query, $matches)) { |
||
794 | if (! isset($matches['upper']) && isset($matches['lower']) && $matches['lower'] == 1) { |
||
795 | // good it's already correctly set. |
||
796 | } else { |
||
797 | // We have a limit, but it's not for a single row |
||
798 | return false; |
||
799 | } |
||
800 | } elseif ($appendLimit) { |
||
801 | $query .= ' LIMIT 1'; |
||
802 | } |
||
803 | |||
804 | $rows = $this->query($query); |
||
805 | if (! $rows || \count($rows) === 0) { |
||
806 | $rows = false; |
||
807 | } |
||
808 | |||
809 | return \is_array($rows) ? $rows[0] : $rows; |
||
810 | } |
||
811 | |||
812 | /** |
||
813 | * Optimises/repairs tables on mysql. |
||
814 | * |
||
815 | * @param bool $admin If we are on web, don't echo. |
||
816 | * @param string $type 'full' | '' Force optimize of all tables. |
||
817 | * 'space' Optimise tables with 5% or more free space. |
||
818 | * 'analyze' Analyze tables to rebuild statistics. |
||
819 | * @param bool|string $local Only analyze local tables. Good if running replication. |
||
820 | * @param array $tableList (optional) Names of tables to analyze. |
||
821 | * |
||
822 | * @return int Quantity optimized/analyzed |
||
823 | * @throws \RuntimeException |
||
824 | */ |
||
825 | public function optimise($admin = false, $type = '', $local = false, $tableList = []) |
||
826 | { |
||
827 | $tableAnd = ''; |
||
828 | if (\count($tableList)) { |
||
829 | foreach ($tableList as $tableName) { |
||
830 | $tableAnd .= ($this->escapeString($tableName).','); |
||
831 | } |
||
832 | $tableAnd = (' AND Name IN ('.rtrim($tableAnd, ',').')'); |
||
833 | } |
||
834 | |||
835 | switch ($type) { |
||
836 | case 'space': |
||
837 | $tableArray = $this->queryDirect('SHOW TABLE STATUS WHERE Data_free / Data_length > 0.005'.$tableAnd); |
||
838 | $myIsamTables = $this->queryDirect("SHOW TABLE STATUS WHERE ENGINE LIKE 'myisam' AND Data_free / Data_length > 0.005".$tableAnd); |
||
839 | break; |
||
840 | case 'analyze': |
||
841 | case '': |
||
842 | case 'full': |
||
843 | default: |
||
844 | $tableArray = $this->queryDirect('SHOW TABLE STATUS WHERE 1=1'.$tableAnd); |
||
845 | $myIsamTables = $this->queryDirect("SHOW TABLE STATUS WHERE ENGINE LIKE 'myisam'".$tableAnd); |
||
846 | break; |
||
847 | } |
||
848 | |||
849 | $optimised = 0; |
||
850 | if ($tableArray instanceof \Traversable && $tableArray->rowCount()) { |
||
851 | $tableNames = ''; |
||
852 | foreach ($tableArray as $table) { |
||
853 | $tableNames .= $table['name'].','; |
||
854 | } |
||
855 | $tableNames = rtrim($tableNames, ','); |
||
856 | |||
857 | $local = $local ? 'LOCAL' : ''; |
||
858 | if ($type === 'analyze') { |
||
859 | $this->queryExec(sprintf('ANALYZE %s TABLE %s', $local, $tableNames)); |
||
860 | $this->logOptimize($admin, 'ANALYZE', $tableNames); |
||
861 | } else { |
||
862 | $this->queryExec(sprintf('OPTIMIZE %s TABLE %s', $local, $tableNames)); |
||
863 | $this->logOptimize($admin, 'OPTIMIZE', $tableNames); |
||
864 | |||
865 | if ($myIsamTables instanceof \Traversable && $myIsamTables->rowCount()) { |
||
866 | $tableNames = ''; |
||
867 | foreach ($myIsamTables as $table) { |
||
868 | $tableNames .= $table['name'].','; |
||
869 | } |
||
870 | $tableNames = rtrim($tableNames, ','); |
||
871 | $this->queryExec(sprintf('REPAIR %s TABLE %s', $local, $tableNames)); |
||
872 | $this->logOptimize($admin, 'REPAIR', $tableNames); |
||
873 | } |
||
874 | $this->queryExec(sprintf('FLUSH %s TABLES', $local)); |
||
875 | } |
||
876 | $optimised = $tableArray->rowCount(); |
||
877 | } |
||
878 | |||
879 | return $optimised; |
||
880 | } |
||
881 | |||
882 | /** |
||
883 | * Log/echo repaired/optimized/analyzed tables. |
||
884 | * |
||
885 | * @param bool $web If we are on web, don't echo. |
||
886 | * @param string $type ANALYZE|OPTIMIZE|REPAIR |
||
887 | * @param string $tables Table names. |
||
888 | * |
||
889 | * @void |
||
890 | */ |
||
891 | private function logOptimize($web, $type, $tables) |
||
892 | { |
||
893 | $message = $type.' ('.$tables.')'; |
||
894 | if ($web === false) { |
||
895 | echo ColorCLI::primary($message); |
||
896 | } |
||
897 | } |
||
898 | |||
899 | /** |
||
900 | * Turns off autocommit until commit() is ran. http://www.php.net/manual/en/pdo.begintransaction.php. |
||
901 | * |
||
902 | * @return bool |
||
903 | */ |
||
904 | public function beginTransaction(): bool |
||
905 | { |
||
906 | return $this->pdo->beginTransaction(); |
||
907 | } |
||
908 | |||
909 | /** |
||
910 | * Commits a transaction. http://www.php.net/manual/en/pdo.commit.php. |
||
911 | * |
||
912 | * @return bool |
||
913 | */ |
||
914 | public function Commit(): bool |
||
915 | { |
||
916 | return $this->pdo->commit(); |
||
917 | } |
||
918 | |||
919 | /** |
||
920 | * Rollback transcations. http://www.php.net/manual/en/pdo.rollback.php. |
||
921 | * |
||
922 | * @return bool |
||
923 | */ |
||
924 | public function Rollback(): bool |
||
925 | { |
||
926 | return $this->pdo->rollBack(); |
||
927 | } |
||
928 | |||
929 | public function setCovers() |
||
930 | { |
||
931 | $path = Settings::settingValue([ |
||
932 | 'section' => 'site', |
||
933 | 'subsection' => 'main', |
||
934 | 'name' => 'coverspath', |
||
935 | ]); |
||
936 | Utility::setCoversConstant($path); |
||
937 | } |
||
938 | |||
939 | public function rowToArray(array $row) |
||
940 | { |
||
941 | $this->settings[$row['setting']] = $row['value']; |
||
942 | } |
||
943 | |||
944 | public function rowsToArray(array $rows) |
||
945 | { |
||
946 | foreach ($rows as $row) { |
||
947 | if (\is_array($row)) { |
||
948 | $this->rowToArray($row); |
||
949 | } |
||
950 | } |
||
951 | |||
952 | return $this->settings; |
||
953 | } |
||
954 | |||
955 | public function settingsUpdate($form) |
||
956 | { |
||
957 | $error = $this->settingsValidate($form); |
||
958 | |||
959 | if ($error === null) { |
||
960 | $sql = $sqlKeys = []; |
||
961 | foreach ($form as $settingK => $settingV) { |
||
962 | $sql[] = sprintf( |
||
963 | 'WHEN %s THEN %s', |
||
964 | $this->escapeString(trim($settingK)), |
||
965 | $this->escapeString(trim($settingV)) |
||
966 | ); |
||
967 | $sqlKeys[] = $this->escapeString(trim($settingK)); |
||
968 | } |
||
969 | |||
970 | $this->queryExec( |
||
971 | sprintf( |
||
972 | 'UPDATE settings SET value = CASE setting %s END WHERE setting IN (%s)', |
||
973 | implode(' ', $sql), |
||
974 | implode(', ', $sqlKeys) |
||
975 | ) |
||
976 | ); |
||
977 | } else { |
||
978 | $form = $error; |
||
979 | } |
||
980 | |||
981 | return $form; |
||
982 | } |
||
983 | |||
984 | protected function settingsValidate(array $fields) |
||
1032 | } |
||
1033 | |||
1034 | /** |
||
1035 | * PHP interpretation of MySQL's from_unixtime method. |
||
1036 | * @param int $utime UnixTime |
||
1037 | * |
||
1038 | * @return string |
||
1039 | */ |
||
1040 | public function from_unixtime($utime): string |
||
1041 | { |
||
1042 | return 'FROM_UNIXTIME('.$utime.')'; |
||
1043 | } |
||
1044 | |||
1045 | /** |
||
1046 | * PHP interpretation of mysql's unix_timestamp method. |
||
1047 | * @param string $date |
||
1048 | * |
||
1049 | * @return int |
||
1050 | */ |
||
1051 | public function unix_timestamp($date): int |
||
1052 | { |
||
1053 | return strtotime($date); |
||
1054 | } |
||
1055 | |||
1056 | /** |
||
1057 | * Get a string for MySQL with a column name in between |
||
1058 | * ie: UNIX_TIMESTAMP(column_name) AS outputName. |
||
1059 | * |
||
1060 | * @param string $column The datetime column. |
||
1061 | * @param string $outputName The name to store the SQL data into. (the word after AS) |
||
1062 | * |
||
1063 | * @return string |
||
1064 | */ |
||
1065 | public function unix_timestamp_column($column, $outputName = 'unix_time'): string |
||
1066 | { |
||
1067 | return 'UNIX_TIMESTAMP('.$column.') AS '.$outputName; |
||
1068 | } |
||
1069 | |||
1070 | /** |
||
1071 | * @return string |
||
1072 | */ |
||
1073 | public function uuid(): string |
||
1074 | { |
||
1075 | return Uuid::uuid4()->toString(); |
||
1076 | } |
||
1077 | |||
1078 | /** |
||
1079 | * Checks whether the connection to the server is working. Optionally restart a new connection. |
||
1080 | * NOTE: Restart does not happen if PDO is not using exceptions (PHP's default configuration). |
||
1081 | * In this case check the return value === false. |
||
1082 | * |
||
1083 | * @param bool $restart Whether an attempt should be made to reinitialise the Db object on failure. |
||
1084 | * |
||
1085 | * @return bool |
||
1086 | * @throws \RuntimeException |
||
1087 | */ |
||
1088 | public function ping($restart = false): ?bool |
||
1098 | } |
||
1099 | } |
||
1100 | |||
1101 | /** |
||
1102 | * Prepares a statement to be run by the Db engine. |
||
1103 | * To run the statement use the returned $statement with ->execute();. |
||
1104 | * |
||
1105 | * Ideally the signature would have array before $options but that causes a strict warning. |
||
1106 | * |
||
1107 | * @param string $query SQL query to run, with optional place holders. |
||
1108 | * @param array $options Driver options. |
||
1109 | * |
||
1110 | * @return false|\PDOstatement on success false on failure. |
||
1111 | * |
||
1112 | * @link http://www.php.net/pdo.prepare.php |
||
1113 | */ |
||
1114 | public function Prepare($query, $options = []) |
||
1115 | { |
||
1116 | try { |
||
1117 | $PDOstatement = $this->pdo->prepare($query, $options); |
||
1118 | } catch (\PDOException $e) { |
||
1119 | echo ColorCLI::error("\n".$e->getMessage()); |
||
1120 | $PDOstatement = false; |
||
1121 | } |
||
1122 | |||
1123 | return $PDOstatement; |
||
1124 | } |
||
1125 | |||
1126 | /** |
||
1127 | * Retrieve db attributes http://us3.php.net/manual/en/pdo.getattribute.php. |
||
1128 | * |
||
1129 | * @param int $attribute |
||
1130 | * |
||
1131 | * @return false|mixed |
||
1132 | */ |
||
1133 | public function getAttribute($attribute) |
||
1134 | { |
||
1135 | $result = false; |
||
1136 | if ($attribute !== '') { |
||
1137 | try { |
||
1138 | $result = $this->pdo->getAttribute($attribute); |
||
1139 | } catch (\PDOException $e) { |
||
1140 | echo ColorCLI::error("\n".$e->getMessage()); |
||
1141 | $result = false; |
||
1142 | } |
||
1143 | } |
||
1144 | |||
1145 | return $result; |
||
1146 | } |
||
1147 | |||
1148 | /** |
||
1149 | * Returns the stored Db version string. |
||
1150 | * |
||
1151 | * @return string |
||
1152 | */ |
||
1153 | public function getDbVersion(): string |
||
1154 | { |
||
1155 | return $this->dbVersion; |
||
1156 | } |
||
1157 | |||
1158 | /** |
||
1159 | * @param string $requiredVersion The minimum version to compare against |
||
1160 | * |
||
1161 | * @return bool|null TRUE if Db version is greater than or eaqual to $requiredVersion, |
||
1162 | * false if not, and null if the version isn't available to check against. |
||
1163 | */ |
||
1164 | public function isDbVersionAtLeast($requiredVersion): ?bool |
||
1165 | { |
||
1166 | if (empty($this->dbVersion)) { |
||
1167 | return null; |
||
1168 | } |
||
1169 | |||
1170 | return version_compare($requiredVersion, $this->dbVersion, '<='); |
||
1171 | } |
||
1172 | |||
1173 | /** |
||
1174 | * Performs the fetch from the Db server and stores the resulting Major.Minor.Version number. |
||
1175 | */ |
||
1176 | private function fetchDbVersion() |
||
1182 | } |
||
1183 | } |
||
1184 | |||
1185 | /** |
||
1186 | * Checks if the query is empty. Cleans the query of whitespace if needed. |
||
1187 | * |
||
1188 | * @param string $query |
||
1189 | * |
||
1190 | * @return bool |
||
1191 | */ |
||
1192 | private function parseQuery(&$query): bool |
||
1199 | } |
||
1200 | } |
||
1201 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths