Completed
Push — master ( 54db38...e6245f )
by Lars
01:29
created

Helper::generateCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.7333
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 8
            $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
        string $idFieldName = null,
184
        string $language = 'de',
185
        string $table = '',
186
        array $whereArray = null,
187
        DB $dbConnection = null,
188
        string $databaseName = null,
189
        bool $useCache = false,
190
        int $cacheTTL = 3600
191
    ): array {
192
        // init
193 2
        $cacheKey = null;
194
195 2
        if ($dbConnection === null) {
196 2
            $dbConnection = DB::getInstance();
197
        }
198
199 2 View Code Duplication
        if ($table === '') {
200
            $debug = new Debug($dbConnection);
201
            $debug->displayError('Invalid table name, table name in empty.', false);
202
203
            return [];
204
        }
205
206 2
        if ($idFieldName === null) {
207
            $idFieldName = 'id';
208
        }
209
210 2
        if ($whereArray === null) {
211
            $whereArray = [];
212
        }
213
214 2
        $whereSQL = $dbConnection->_parseArrayPair($whereArray, 'AND');
215 2
        if ($whereSQL) {
216 2
            $whereSQL = 'AND ' . $whereSQL;
217
        }
218
219 2
        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
        }
222
223
        // get the row
224 2
        $query = 'SELECT ' . $dbConnection->quote_string($searchFieldName) . ', ' . $dbConnection->quote_string($idFieldName) . ' 
225 2
          FROM ' . $databaseName . $dbConnection->quote_string($table) . '
226
          WHERE 1 = 1
227 2
          ' . $whereSQL . '
228
        ';
229
230 2 View Code Duplication
        if ($useCache) {
231 1
            $cache = new Cache(null, null, false, $useCache);
232 1
            $cacheKey = 'sql-phonetic-search-' . \md5($query);
233
234
            if (
235 1
                $cache->getCacheIsReady()
236
                &&
237 1
                $cache->existsItem($cacheKey)
238
            ) {
239 1
                return $cache->getItem($cacheKey);
240
            }
241
        } else {
242 1
            $cache = false;
243
        }
244
245 2
        $result = $dbConnection->query($query);
246
247 2
        if (!$result instanceof Result) {
248
            return [];
249
        }
250
251
        // make sure the row exists
252 2
        if ($result->num_rows <= 0) {
253
            return [];
254
        }
255
256 2
        $dataToSearchIn = [];
257
        /** @noinspection LoopWhichDoesNotLoopInspection */
258
        /** @noinspection PhpAssignmentInConditionInspection */
259 2
        while ($tmpArray = $result->fetchArray()) {
260 2
            $dataToSearchIn[$tmpArray[$idFieldName]] = $tmpArray[$searchFieldName];
261
        }
262
263 2
        $phonetic = new Phonetic($language);
264 2
        $return = $phonetic->phonetic_matches($searchString, $dataToSearchIn);
265
266
        // save into the cache
267 View Code Duplication
        if (
268 2
            $cacheKey !== null
269
            &&
270 2
            $useCache
271
            &&
272 2
            $cache instanceof Cache
273
            &&
274 2
            $cache->getCacheIsReady()
275
        ) {
276 1
            $cache->setItem($cacheKey, $return, $cacheTTL);
277
        }
278
279 2
        return $return;
280
    }
281
282
    /**
283
     * A string that represents the MySQL client library version.
284
     *
285
     * @param DB $dbConnection
286
     *
287
     * @return string
288
     */
289 8 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 8
        static $MYSQL_CLIENT_VERSION_CACHE = [];
292
293 8
        if ($dbConnection === null) {
294
            $dbConnection = DB::getInstance();
295
        }
296
297 8
        $cacheKey = self::generateCacheKey($dbConnection);
298
299 8
        if (isset($MYSQL_CLIENT_VERSION_CACHE[$cacheKey])) {
300 8
            return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey];
301
        }
302
303 2
        $doctrineConnection = $dbConnection->getDoctrineConnection();
304 2
        if ($doctrineConnection) {
305
            $doctrineWrappedConnection = $doctrineConnection->getWrappedConnection();
306
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
307
                return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey] = $doctrineWrappedConnection->getAttribute(5); // 5 = PDO::ATTR_CLIENT_VERSION
308
            }
309
        }
310
311 2
        $mysqli_link = $dbConnection->getLink();
312 2
        if (!$mysqli_link) {
313
            return '';
314
        }
315
316 2
        return $MYSQL_CLIENT_VERSION_CACHE[$cacheKey] = (string) \mysqli_get_client_version($mysqli_link);
317
    }
318
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
     */
326 8 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 8
        static $MYSQL_SERVER_VERSION_CACHE = [];
329
330 8
        if ($dbConnection === null) {
331
            $dbConnection = DB::getInstance();
332
        }
333
334 8
        $cacheKey = self::generateCacheKey($dbConnection);
335
336 8
        if (isset($MYSQL_SERVER_VERSION_CACHE[$cacheKey])) {
337 8
            return $MYSQL_SERVER_VERSION_CACHE[$cacheKey];
338
        }
339
340 2
        $doctrineConnection = $dbConnection->getDoctrineConnection();
341 2
        if ($doctrineConnection) {
342
            $doctrineWrappedConnection = $doctrineConnection->getWrappedConnection();
343
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
344
                return $MYSQL_SERVER_VERSION_CACHE[$cacheKey] = (string) $doctrineWrappedConnection->getServerVersion();
345
            }
346
        }
347
348 2
        $mysqli_link = $dbConnection->getLink();
349 2
        if (!$mysqli_link) {
350
            return '';
351
        }
352
353 2
        return $MYSQL_SERVER_VERSION_CACHE[$cacheKey] = (string) \mysqli_get_server_version($mysqli_link);
354
    }
355
356
    /**
357
     * Return all db-fields from a table.
358
     *
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
     */
366 1
    public static function getDbFields(string $table, bool $useStaticCache = true, DB $dbConnection = null, string $databaseName = null): array
367
    {
368 1
        static $DB_FIELDS_CACHE = [];
369
370
        // use the static cache
371
        if (
372 1
            $useStaticCache
373
            &&
374 1
            isset($DB_FIELDS_CACHE[$table])
375
        ) {
376 1
            return $DB_FIELDS_CACHE[$table];
377
        }
378
379
        // init
380 1
        $dbFields = [];
381
382 1
        if ($dbConnection === null) {
383 1
            $dbConnection = DB::getInstance();
384
        }
385
386 1 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 1
        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 1
        $result = $dbConnection->query($sql);
399
400 1
        if ($result instanceof Result && $result->num_rows > 0) {
401 1
            foreach ($result->fetchAllArray() as $tmpResult) {
402 1
                $dbFields[] = $tmpResult['Field'];
403
            }
404
        }
405
406
        // add to static cache
407 1
        $DB_FIELDS_CACHE[$table] = $dbFields;
408
409 1
        return $dbFields;
410
    }
411
412
    /**
413
     * Copy row within a DB table and making updates to the columns.
414
     *
415
     * @param string      $table
416
     * @param array       $whereArray
417
     * @param array       $updateArray
418
     * @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
     *                   "false" on error
424
     */
425 1
    public static function copyTableRow(
426
        string $table,
427
        array $whereArray,
428
        array $updateArray = [],
429
        array $ignoreArray = [],
430
        DB $dbConnection = null,
431
        string $databaseName = null
432
    ) {
433
        // init
434 1
        $table = \trim($table);
435
436 1
        if ($dbConnection === null) {
437 1
            $dbConnection = DB::getInstance();
438
        }
439
440 1 View Code Duplication
        if ($table === '') {
441
            $debug = new Debug($dbConnection);
442
            $debug->displayError('Invalid table name, table name in empty.', false);
443
444
            return false;
445
        }
446
447 1
        $whereSQL = $dbConnection->_parseArrayPair($whereArray, 'AND');
448 1
        if ($whereSQL) {
449 1
            $whereSQL = 'AND ' . $whereSQL;
450
        }
451
452 1
        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
456
        // get the row
457 1
        $query = 'SELECT * FROM ' . $databaseName . $dbConnection->quote_string($table) . '
458
          WHERE 1 = 1
459 1
          ' . $whereSQL . '
460
        ';
461 1
        $result = $dbConnection->query($query);
462
463
        // make sure the row exists
464 1
        if ($result instanceof Result && $result->num_rows > 0) {
465
466
            /** @noinspection LoopWhichDoesNotLoopInspection */
467
            /** @noinspection PhpAssignmentInConditionInspection */
468 1
            while ($tmpArray = $result->fetchArray()) {
469
470
                // re-build a new DB query and ignore some field-names
471 1
                $bindings = [];
472 1
                $insert_keys = '';
473 1
                $insert_values = '';
474
475 1
                foreach ($tmpArray as $fieldName => $value) {
476 1
                    if (!\in_array($fieldName, $ignoreArray, true)) {
477 1
                        if (\array_key_exists($fieldName, $updateArray)) {
478 1
                            $insert_keys .= ',' . $fieldName;
479 1
                            $insert_values .= ',?';
480 1
                            $bindings[] = $updateArray[$fieldName]; // INFO: do not escape non selected data
481
                        } else {
482 1
                            $insert_keys .= ',' . $fieldName;
483 1
                            $insert_values .= ',?';
484 1
                            $bindings[] = $value; // INFO: do not escape non selected data
485
                        }
486
                    }
487
                }
488
489 1
                $insert_keys = \ltrim($insert_keys, ',');
490 1
                $insert_values = \ltrim($insert_values, ',');
491
492
                // insert the "copied" row
493 1
                $new_query = 'INSERT INTO ' . $databaseName . $dbConnection->quote_string($table) . ' 
494 1
                  (' . $insert_keys . ')
495
                  VALUES 
496 1
                  (' . $insert_values . ')
497
                ';
498
499 1
                $return = $dbConnection->query($new_query, $bindings);
500 1
                \assert(\is_int($return) || \is_string($return) || $return === false);
501
502 1
                return $return;
503
            }
504
        }
505
506
        return false;
507
    }
508
}
509