Completed
Push — master ( 39d9a4...0a738c )
by Lars
03:51
created

Helper::repairTables()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 19

Duplication

Lines 19
Ratio 100 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
dl 19
loc 19
ccs 11
cts 11
cp 1
rs 9.3222
c 0
b 0
f 0
cc 5
nc 4
nop 2
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use voku\cache\Cache;
8
use voku\helper\Phonetic;
9
10
/**
11
 * Helper: This class can handle extra functions that use the "Simple-MySQLi"-classes.
12
 */
13
class Helper
14
{
15
    /**
16
     * @param DB $dbConnection
17
     *
18
     * @return string
19
     */
20 8
    private static function generateCacheKey(DB $dbConnection): string
21
    {
22
        // init
23 8
        $configTmp = [];
24
25 8
        $configOrig = $dbConnection->getConfig();
26 8
        \array_walk_recursive(
27
            $configOrig,
28
            static function ($k, $v) use (&$configTmp) {
29 8
                $configTmp[] = $v;
30 8
                $configTmp[] = $k;
31 8
            }
32
        );
33
34 8
        return \implode('--', $configTmp);
35
    }
36
37
    /**
38
     * Optimize tables
39
     *
40
     * @param array   $tables       database table names
41
     * @param DB|null $dbConnection <p>Use <strong>null</strong> to get your first singleton instance.</p>
42
     *
43
     * @return int
44
     */
45 1 View Code Duplication
    public static function optimizeTables(array $tables = [], DB $dbConnection = null): int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
46
    {
47 1
        if ($dbConnection === null) {
48 1
            $dbConnection = DB::getInstance();
49
        }
50
51 1
        $optimized = 0;
52 1
        if (!empty($tables)) {
53 1
            foreach ($tables as $table) {
54 1
                $optimize = 'OPTIMIZE TABLE ' . $dbConnection->quote_string($table);
55 1
                $result = $dbConnection->query($optimize);
56 1
                if ($result) {
57 1
                    $optimized++;
58
                }
59
            }
60
        }
61
62 1
        return $optimized;
63
    }
64
65
    /**
66
     * Repair tables
67
     *
68
     * @param array   $tables       database table names
69
     * @param DB|null $dbConnection <p>Use <strong>null</strong> to get your first singleton instance.</p>
70
     *
71
     * @return int
72
     */
73 1 View Code Duplication
    public static function repairTables(array $tables = [], DB $dbConnection = null): int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
74
    {
75 1
        if ($dbConnection === null) {
76 1
            $dbConnection = DB::getInstance();
77
        }
78
79 1
        $optimized = 0;
80 1
        if (!empty($tables)) {
81 1
            foreach ($tables as $table) {
82 1
                $optimize = 'REPAIR TABLE ' . $dbConnection->quote_string($table);
83 1
                $result = $dbConnection->query($optimize);
84 1
                if ($result) {
85 1
                    $optimized++;
86
                }
87
            }
88
        }
89
90 1
        return $optimized;
91
    }
92
93
    /**
94
     * Check if "mysqlnd"-driver is used.
95
     *
96
     * @return bool
97
     */
98 91
    public static function isMysqlndIsUsed(): bool
99
    {
100 91
        static $_mysqlnd_is_used = null;
101
102 91
        if ($_mysqlnd_is_used === null) {
103
            $_mysqlnd_is_used = (
104 1
                \extension_loaded('mysqlnd')
105
                &&
106 1
                \function_exists('mysqli_fetch_all')
107
            );
108
        }
109
110 91
        return $_mysqlnd_is_used;
111
    }
112
113
    /**
114
     * Check if the current environment supports "utf8mb4".
115
     *
116
     * @param DB $dbConnection
117
     *
118
     * @return bool
119
     */
120 8
    public static function isUtf8mb4Supported(DB $dbConnection = null): bool
121
    {
122
        /**
123
         *  https://make.wordpress.org/core/2015/04/02/the-utf8mb4-upgrade/
124
         *
125
         * - You’re currently using the utf8 character set.
126
         * - Your MySQL server is version 5.5.3 or higher (including all 10.x versions of MariaDB).
127
         * - Your MySQL client libraries are version 5.5.3 or higher. If you’re using mysqlnd, 5.0.9 or higher.
128
         *
129
         * INFO: utf8mb4 is 100% backwards compatible with utf8.
130
         */
131 8
        if ($dbConnection === null) {
132
            $dbConnection = DB::getInstance();
133
        }
134
135 8
        $server_version = self::get_mysql_server_version($dbConnection);
136 8
        $client_version = self::get_mysql_client_version($dbConnection);
137
138
        if (
139 8
            $server_version >= 50503
140
            &&
141
            (
142
                (
143 8
                    self::isMysqlndIsUsed()
144
                    &&
145 8
                    $client_version >= 50009
146
                )
147
                ||
148
                (
149
                    !self::isMysqlndIsUsed()
150
                    &&
151 8
                    $client_version >= 50503
152
                )
153
            )
154
155
        ) {
156 8
            return true;
157
        }
158
159
        return false;
160
    }
161
162
    /**
163
     * A phonetic search algorithms for different languages.
164
     *
165
     * INFO: if you need better performance, please save the "voku\helper\Phonetic"-output into the DB and search for it
166
     *
167
     * @param string      $searchString
168
     * @param string      $searchFieldName
169
     * @param string      $idFieldName
170
     * @param string      $language     <p>en, de, fr</p>
171
     * @param string      $table
172
     * @param array       $whereArray
173
     * @param DB|null     $dbConnection <p>use <strong>null</strong> if you will use the current database-connection</p>
174
     * @param string|null $databaseName <p>use <strong>null</strong> if you will use the current database</p>
175
     * @param bool        $useCache     use cache?
176
     * @param int         $cacheTTL     cache-ttl in seconds
177
     *
178
     * @return array
179
     */
180 2
    public static function phoneticSearch(
181
        string $searchString,
182
        string $searchFieldName,
183 2
        string $idFieldName = null,
184
        string $language = 'de',
185 2
        string $table = '',
186 2
        array $whereArray = null,
187
        DB $dbConnection = null,
188
        string $databaseName = null,
189 2
        bool $useCache = false,
190
        int $cacheTTL = 3600
191
    ): array {
192
        // init
193
        $cacheKey = null;
194
195
        if ($dbConnection === null) {
196 2
            $dbConnection = DB::getInstance();
197
        }
198
199 View Code Duplication
        if ($table === '') {
200 2
            $debug = new Debug($dbConnection);
201 2
            $debug->displayError('Invalid table name, table name in empty.', false);
202 2
203
            return [];
204
        }
205 2
206
        if ($idFieldName === null) {
207
            $idFieldName = 'id';
208
        }
209
210 2
        if ($whereArray === null) {
211 2
            $whereArray = [];
212
        }
213 2
214
        $whereSQL = $dbConnection->_parseArrayPair($whereArray, 'AND');
215
        if ($whereSQL) {
216 2
            $whereSQL = 'AND ' . $whereSQL;
217 1
        }
218 1
219
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
220
            $databaseName = $dbConnection->quote_string(\trim($databaseName)) . '.';
221 1
        }
222
223 1
        // get the row
224
        $query = 'SELECT ' . $dbConnection->quote_string($searchFieldName) . ', ' . $dbConnection->quote_string($idFieldName) . ' 
225 1
          FROM ' . $databaseName . $dbConnection->quote_string($table) . '
226
          WHERE 1 = 1
227
          ' . $whereSQL . '
228 1
        ';
229
230 View Code Duplication
        if ($useCache) {
231 2
            $cache = new Cache(null, null, false, $useCache);
232
            $cacheKey = 'sql-phonetic-search-' . \md5($query);
233
234 2
            if (
235
                $cache->getCacheIsReady()
236
                &&
237
                $cache->existsItem($cacheKey)
238 2
            ) {
239
                return $cache->getItem($cacheKey);
240
            }
241 2
        } else {
242 2
            $cache = false;
243
        }
244
245 2
        $result = $dbConnection->query($query);
246 2
247
        if (!$result instanceof Result) {
248
            return [];
249
        }
250 2
251
        // make sure the row exists
252 2
        if ($result->num_rows <= 0) {
253
            return [];
254 2
        }
255
256 2
        $dataToSearchIn = [];
257
        /** @noinspection LoopWhichDoesNotLoopInspection */
258 1
        /** @noinspection PhpAssignmentInConditionInspection */
259
        while ($tmpArray = $result->fetchArray()) {
260
            $dataToSearchIn[$tmpArray[$idFieldName]] = $tmpArray[$searchFieldName];
261 2
        }
262
263
        $phonetic = new Phonetic($language);
264
        $return = $phonetic->phonetic_matches($searchString, $dataToSearchIn);
265
266
        // save into the cache
267 View Code Duplication
        if (
268
            $cacheKey !== null
269
            &&
270
            $useCache
271 8
            &&
272
            $cache instanceof Cache
273 8
            &&
274
            $cache->getCacheIsReady()
275 8
        ) {
276
            $cache->setItem($cacheKey, $return, $cacheTTL);
277
        }
278
279 8
        return $return;
280
    }
281 8
282 8
    /**
283
     * A string that represents the MySQL client library version.
284
     *
285 2
     * @param DB $dbConnection
286 2
     *
287
     * @return string
288
     */
289 View Code Duplication
    public static function get_mysql_client_version(DB $dbConnection = null): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
290
    {
291
        static $MYSQL_CLIENT_VERSION_CACHE = [];
292
293 2
        if ($dbConnection === null) {
294
            $dbConnection = DB::getInstance();
295
        }
296
297
        $cacheKey = self::generateCacheKey($dbConnection);
298
299
        if (isset($MYSQL_CLIENT_VERSION_CACHE[$cacheKey])) {
300
            return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey];
301
        }
302
303 8
        $doctrineConnection = $dbConnection->getDoctrineConnection();
304
        if ($doctrineConnection) {
305 8
            $doctrineWrappedConnection = $doctrineConnection->getWrappedConnection();
306
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
307 8
                return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey] = $doctrineWrappedConnection->getAttribute(5); // 5 = PDO::ATTR_CLIENT_VERSION
308
            }
309
        }
310
311 8
        $mysqli_link = $dbConnection->getLink();
312
        if (!$mysqli_link) {
313 8
            return '';
314 8
        }
315
316
        return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey] = (string) \mysqli_get_client_version($mysqli_link);
317 2
    }
318 2
319
    /**
320
     * Returns a string representing the version of the MySQL server that the MySQLi extension is connected to.
321
     *
322
     * @param DB $dbConnection
323
     *
324
     * @return string
325 2
     */
326 View Code Duplication
    public static function get_mysql_server_version(DB $dbConnection = null): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
327
    {
328
        static $MYSQL_SERVER_VERSION_CACHE = [];
329
330
        if ($dbConnection === null) {
331
            $dbConnection = DB::getInstance();
332
        }
333
334
        $cacheKey = self::generateCacheKey($dbConnection);
335
336
        if (isset($MYSQL_SERVER_VERSION_CACHE[$cacheKey])) {
337
            return $MYSQL_SERVER_VERSION_CACHE[$cacheKey];
338 1
        }
339
340 1
        $doctrineConnection = $dbConnection->getDoctrineConnection();
341
        if ($doctrineConnection) {
342
            $doctrineWrappedConnection = $doctrineConnection->getWrappedConnection();
343
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
344 1
                return $MYSQL_SERVER_VERSION_CACHE[$cacheKey] = (string) $doctrineWrappedConnection->getServerVersion();
345
            }
346 1
        }
347
348 1
        $mysqli_link = $dbConnection->getLink();
349
        if (!$mysqli_link) {
350
            return '';
351
        }
352 1
353
        return $MYSQL_SERVER_VERSION_CACHE[$cacheKey] = (string) \mysqli_get_server_version($mysqli_link);
354 1
    }
355 1
356
    /**
357
     * Return all db-fields from a table.
358 1
     *
359
     * @param string      $table
360
     * @param bool        $useStaticCache
361
     * @param DB|null     $dbConnection <p>use <strong>null</strong> if you will use the current database-connection</p>
362
     * @param string|null $databaseName <p>use <strong>null</strong> if you will use the current database</p>
363
     *
364
     * @return array
365 1
     */
366
    public static function getDbFields(string $table, bool $useStaticCache = true, DB $dbConnection = null, string $databaseName = null): array
367
    {
368
        static $DB_FIELDS_CACHE = [];
369 1
370 1
        // use the static cache
371
        if (
372 1
            $useStaticCache
373 1
            &&
374 1
            isset($DB_FIELDS_CACHE[$table])
375
        ) {
376
            return $DB_FIELDS_CACHE[$table];
377
        }
378
379 1
        // init
380
        $dbFields = [];
381 1
382
        if ($dbConnection === null) {
383
            $dbConnection = DB::getInstance();
384
        }
385
386 View Code Duplication
        if ($table === '') {
387
            $debug = new Debug($dbConnection);
388
            $debug->displayError('Invalid table name, table name in empty.', false);
389
390
            return [];
391
        }
392
393
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
394
            $databaseName = $dbConnection->quote_string(\trim($databaseName)) . '.';
395
        }
396
397 1
        $sql = 'SHOW COLUMNS FROM ' . $databaseName . $dbConnection->escape($table);
398
        $result = $dbConnection->query($sql);
399
400 1
        if ($result instanceof Result && $result->num_rows > 0) {
401
            foreach ($result->fetchAllArray() as $tmpResult) {
402 1
                $dbFields[] = $tmpResult['Field'];
403 1
            }
404
        }
405
406 1
        // add to static cache
407
        $DB_FIELDS_CACHE[$table] = $dbFields;
408
409
        return $dbFields;
410
    }
411
412
    /**
413 1
     * Copy row within a DB table and making updates to the columns.
414 1
     *
415 1
     * @param string      $table
416
     * @param array       $whereArray
417
     * @param array       $updateArray
418 1
     * @param array       $ignoreArray
419
     * @param DB|null     $dbConnection <p>Use <strong>null</strong> to get your first singleton instance.</p>
420
     * @param string|null $databaseName <p>use <strong>null</strong> if you will use the current database</p>
421
     *
422
     * @return false|int|string "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
423 1
     *                   "false" on error
424
     */
425 1
    public static function copyTableRow(
426
        string $table,
427 1
        array $whereArray,
428
        array $updateArray = [],
429
        array $ignoreArray = [],
430 1
        DB $dbConnection = null,
431
        string $databaseName = null
432
    ) {
433
        // init
434 1
        $table = \trim($table);
435
436
        if ($dbConnection === null) {
437 1
            $dbConnection = DB::getInstance();
438 1
        }
439 1
440 View Code Duplication
        if ($table === '') {
441 1
            $debug = new Debug($dbConnection);
442 1
            $debug->displayError('Invalid table name, table name in empty.', false);
443 1
444 1
            return false;
445 1
        }
446 1
447
        $whereSQL = $dbConnection->_parseArrayPair($whereArray, 'AND');
448 1
        if ($whereSQL) {
449 1
            $whereSQL = 'AND ' . $whereSQL;
450 1
        }
451
452
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
453
            $databaseName = $dbConnection->quote_string(\trim($databaseName)) . '.';
454
        }
455 1
456 1
        // get the row
457
        $query = 'SELECT * FROM ' . $databaseName . $dbConnection->quote_string($table) . '
458
          WHERE 1 = 1
459 1
          ' . $whereSQL . '
460 1
        ';
461
        $result = $dbConnection->query($query);
462 1
463
        // make sure the row exists
464
        if ($result instanceof Result && $result->num_rows > 0) {
465 1
466
            /** @noinspection LoopWhichDoesNotLoopInspection */
467
            /** @noinspection PhpAssignmentInConditionInspection */
468
            while ($tmpArray = $result->fetchArray()) {
469
470
                // re-build a new DB query and ignore some field-names
471
                $bindings = [];
472
                $insert_keys = '';
473
                $insert_values = '';
474
475
                foreach ($tmpArray as $fieldName => $value) {
476
                    if (!\in_array($fieldName, $ignoreArray, true)) {
477
                        if (\array_key_exists($fieldName, $updateArray)) {
478
                            $insert_keys .= ',' . $fieldName;
479
                            $insert_values .= ',?';
480
                            $bindings[] = $updateArray[$fieldName]; // INFO: do not escape non selected data
481
                        } else {
482
                            $insert_keys .= ',' . $fieldName;
483
                            $insert_values .= ',?';
484
                            $bindings[] = $value; // INFO: do not escape non selected data
485
                        }
486
                    }
487
                }
488
489
                $insert_keys = \ltrim($insert_keys, ',');
490
                $insert_values = \ltrim($insert_values, ',');
491
492
                // insert the "copied" row
493
                $new_query = 'INSERT INTO ' . $databaseName . $dbConnection->quote_string($table) . ' 
494
                  (' . $insert_keys . ')
495
                  VALUES 
496
                  (' . $insert_values . ')
497
                ';
498
499
                $return = $dbConnection->query($new_query, $bindings);
500
                \assert(\is_int($return) || \is_string($return) || $return === false);
501
502
                return $return;
503
            }
504
        }
505
506
        return false;
507
    }
508
}
509