MySqlDataLayer   F
last analyzed

Complexity

Total Complexity 143

Size/Duplication

Total Lines 1213
Duplicated Lines 0 %

Test Coverage

Coverage 85.62%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 305
dl 0
loc 1213
ccs 250
cts 292
cp 0.8562
rs 2
c 5
b 0
f 0
wmc 143

40 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A bindAssoc() 0 23 4
A quoteFloat() 0 8 2
A quoteDecimal() 0 13 5
A connectIfNotAlive() 0 5 2
A executeRow0() 0 18 4
A executeNone() 0 12 2
A getQueryLog() 0 3 1
B executeTable() 0 69 10
A executeSingleton1() 0 18 3
B executeLog() 0 45 9
A executeBulk() 0 18 3
A executeRows() 0 12 2
A executeRow1() 0 18 3
A quoteBit() 0 8 3
A begin() 0 6 2
A commit() 0 6 2
A quoteInt() 0 8 2
A quoteBinary() 0 8 3
A getMaxAllowedPacket() 0 17 2
A connect() 0 26 4
A executeMulti() 0 34 6
A isAlive() 0 3 1
A executeSingleton0() 0 23 4
A disconnect() 0 4 1
A realQuery() 0 24 5
A setConnector() 0 8 2
D executeTableShowTableColumn() 0 31 18
A multiQuery() 0 23 5
B quoteListOfInt() 0 21 8
A showWarnings() 0 3 1
A rollback() 0 6 2
A query() 0 26 5
A dataLayerError() 0 3 1
A quoteString() 0 8 3
A queryError() 0 3 1
A sendLongData() 0 14 4
A executeTableShowHeader() 0 22 4
A executeTableShowFooter() 0 9 2
A realEscapeString() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like MySqlDataLayer 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 MySqlDataLayer, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace SetBased\Stratum\MySql;
5
6
use SetBased\Exception\FallenException;
7
use SetBased\Exception\LogicException;
8
use SetBased\Stratum\Middle\BulkHandler;
9
use SetBased\Stratum\Middle\Exception\ResultException;
10
use SetBased\Stratum\MySql\Exception\MySqlConnectFailedException;
11
use SetBased\Stratum\MySql\Exception\MySqlDataLayerException;
12
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException;
13
14
/**
15
 * Supper class for routine wrapper classes.
16
 */
17
class MySqlDataLayer
18
{
19
  //--------------------------------------------------------------------------------------------------------------------
20
  /**
21
   * The default character set to be used when sending data from and to the MySQL instance.
22
   *
23
   * @var string
24
   *
25
   * @since 1.0.0
26
   * @api
27
   */
28
  public string $charSet = 'utf8mb4';
29
30
  /**
31
   * Whether queries must be logged.
32
   *
33
   * @var bool
34
   *
35
   * @since 1.0.0
36
   * @api
37
   */
38
  public bool $logQueries = false;
39
40
  /**
41
   * The options to be set.
42
   *
43
   * @var array
44
   */
45
  public array $options = [MYSQLI_OPT_INT_AND_FLOAT_NATIVE => true];
46
47
  /**
48
   * The SQL mode of the MySQL instance.
49
   *
50
   * @var string
51
   *
52
   * @since 1.0.0
53
   * @api
54
   */
55
  public string $sqlMode = 'STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_VALUE_ON_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY';
56
57
  /**
58
   * The transaction isolation level. Possible values are:
59
   * <ul>
60
   * <li> REPEATABLE READ
61
   * <li> READ COMMITTED
62
   * <li> READ UNCOMMITTED
63
   * <li> SERIALIZABLE
64
   * </ul>
65
   *
66
   * @var string
67
   *
68
   * @since 1.0.0
69
   * @api
70
   */
71
  public string $transactionIsolationLevel = 'READ COMMITTED';
72
73
  /**
74
   * Chunk size when transmitting LOB to the MySQL instance. Must be less than max_allowed_packet.
75
   *
76
   * @var int
77
   */
78
  protected int $chunkSize;
79
80
  /**
81
   * Value of variable max_allowed_packet
82
   *
83
   * @var int|null
84
   */
85
  protected ?int $maxAllowedPacket = null;
86
87
  /**
88
   * The connection between PHP and the MySQL or MariaDB instance.
89
   *
90
   * @var \mysqli|null
91
   */
92
  protected ?\mysqli $mysqli = null;
93
94
  /**
95
   * The query log.
96
   *
97
   * @var array[]
98
   */
99
  protected array $queryLog = [];
100
101
  /**
102
   * The object for connecting to a MySQL or MariaDB instance.
103
   *
104
   * @var MySqlConnector
105
   */
106
  private MySqlConnector $connector;
107
108
  //--------------------------------------------------------------------------------------------------------------------
109
  /**
110
   * MySqlDataLayer constructor.
111
   *
112
   * @param MySqlConnector $connector The object for connecting to a MySQL or MariaDB instance.
113
   */
114 119
  public function __construct(MySqlConnector $connector)
115
  {
116 119
    $this->connector = $connector;
117
  }
118
119
  //--------------------------------------------------------------------------------------------------------------------
120
  /**
121
   * Starts a transaction.
122
   *
123
   * MysqlWrapper around [mysqli::autocommit](http://php.net/manual/mysqli.autocommit.php), however on failure an
124
   * exception is thrown.
125
   *
126
   * @throws MySqlDataLayerException
127
   *
128
   * @api
129
   * @since 1.0.0
130
   */
131
  public function begin(): void
132
  {
133
    $success = @$this->mysqli->autocommit(false);
134
    if (!$success)
135
    {
136
      throw $this->dataLayerError('mysqli::autocommit');
137
    }
138
  }
139
140
  //--------------------------------------------------------------------------------------------------------------------
141
  /**
142
   * @param \mysqli_stmt $stmt
143
   * @param array        $out
144 30
   *
145
   * @throws MySqlDataLayerException
146 30
   */
147 30
  public function bindAssoc(\mysqli_stmt $stmt, array &$out): void
148
  {
149 30
    $data = $stmt->result_metadata();
150 30
    if (!$data)
0 ignored issues
show
introduced by
$data is of type mysqli_result, thus it always evaluated to true.
Loading history...
151
    {
152 30
      throw $this->dataLayerError('mysqli_stmt::result_metadata');
153
    }
154 30
155
    $fields = [];
156
    $out    = [];
157 30
158 30
    while (($field = $data->fetch_field()))
159
    {
160 30
      $fields[] = &$out[$field->name];
161
    }
162
163
    $b = call_user_func_array([$stmt, 'bind_result'], $fields);
164
    if ($b===false)
165
    {
166
      throw $this->dataLayerError('mysqli_stmt::bind_result');
167
    }
168
169
    $data->free();
170
  }
171
172
  //--------------------------------------------------------------------------------------------------------------------
173
  /**
174
   * Commits the current transaction (and starts a new transaction).
175
   *
176
   * MysqlWrapper around [mysqli::commit](http://php.net/manual/mysqli.commit.php), however on failure an exception is
177
   * thrown.
178
   *
179
   * @throws MySqlDataLayerException
180
   *
181
   * @api
182
   * @since 1.0.0
183
   */
184
  public function commit(): void
185
  {
186
    $success = @$this->mysqli->commit();
187
    if (!$success)
188
    {
189
      throw $this->dataLayerError('mysqli::commit');
190
    }
191 119
  }
192
193 119
  //--------------------------------------------------------------------------------------------------------------------
194
  /**
195
   * Connects PHP to the MySQL instance.
196 119
   *
197
   * @throws MySqlConnectFailedException
198 119
   * @throws MySqlDataLayerException
199 119
   *
200
   * @since 1.0.0
201
   * @api
202
   */
203 119
  public function connect(): void
204 119
  {
205
    $this->mysqli = $this->connector->connect();
206
207 119
    // Set the options.
208
    foreach ($this->options as $option => $value)
209
    {
210 119
      $success = @$this->mysqli->options($option, $value);
211
      if (!$success)
212
      {
213
        throw $this->dataLayerError('mysqli::options');
214
      }
215
    }
216
217
    // Set the default character set.
218
    $success = @$this->mysqli->set_charset($this->charSet);
219
    if (!$success)
220
    {
221
      throw $this->dataLayerError('mysqli::set_charset');
222
    }
223 1
224
    // Set the SQL mode.
225 1
    $this->executeNone("set sql_mode = '".$this->sqlMode."'");
226
227 1
    // Set transaction isolation level.
228
    $this->executeNone('set session transaction isolation level '.$this->transactionIsolationLevel);
229
  }
230
231
  //--------------------------------------------------------------------------------------------------------------------
232
  /**
233
   * Connects or reconnects to the MySQL instance when PHP is not (longer) connected to a MySQL or MariaDB instance.
234
   *
235
   * @throws MySqlConnectFailedException
236
   * @throws MySqlDataLayerException
237
   *
238 2
   * @since 5.0.0
239
   * @api
240 2
   */
241 2
  public function connectIfNotAlive(): void
242
  {
243
    if (!$this->connector->isAlive())
244
    {
245
      $this->mysqli = $this->connector->connect();
246
    }
247
  }
248
249
  //--------------------------------------------------------------------------------------------------------------------
250
  /**
251
   * Closes the connection to the MySQL instance, if connected.
252
   *
253
   * @since 1.0.0
254
   * @api
255
   */
256
  public function disconnect(): void
257
  {
258
    $this->mysqli = null;
259
    $this->connector->disconnect();
260
  }
261
262
  //--------------------------------------------------------------------------------------------------------------------
263
  /**
264
   * Executes a query using a bulk handler.
265
   *
266
   * @param BulkHandler $bulkHandler The bulk handler.
267
   * @param string      $query       The SQL statement.
268
   *
269
   * @throws MySqlQueryErrorException
270
   *
271
   * @api
272
   * @since 1.0.0
273
   */
274
  public function executeBulk(BulkHandler $bulkHandler, string $query): void
275
  {
276
    $this->realQuery($query);
277
278
    $bulkHandler->start();
279
280
    $result = $this->mysqli->use_result();
281
    while (($row = $result->fetch_assoc()))
282
    {
283
      $bulkHandler->row($row);
284
    }
285
    $result->free();
286
287 1
    $bulkHandler->stop();
288
289
    if ($this->mysqli->more_results())
290 1
    {
291
      $this->mysqli->next_result();
292 1
    }
293
  }
294
295 1
  //--------------------------------------------------------------------------------------------------------------------
296 1
  /**
297 1
   * Executes a query and logs the result set.
298
   *
299 1
   * @param string $queries The query or multi query.
300 1
   *
301
   * @return int The total number of rows selected/logged.
302 1
   *
303 1
   * @throws MySqlDataLayerException
304
   *
305 1
   * @api
306 1
   * @since 1.0.0
307
   */
308 1
  public function executeLog(string $queries): int
309 1
  {
310
    // Counter for the number of rows written/logged.
311 1
    $n = 0;
312
313
    $this->multiQuery($queries);
314 1
    do
315 1
    {
316
      $result = @$this->mysqli->store_result();
317 1
      if ($this->mysqli->errno)
318 1
      {
319
        throw $this->dataLayerError('mysqli::store_result');
320
      }
321
      if ($result)
322 1
      {
323
        $fields = $result->fetch_fields();
324
        while (($row = $result->fetch_row()))
325
        {
326
          $line = '';
327
          foreach ($row as $i => $field)
328
          {
329
            if ($i>0)
330
            {
331
              $line .= ' ';
332
            }
333
            $line .= str_pad((string)$field, $fields[$i]->max_length);
334
          }
335
          echo date('Y-m-d H:i:s'), ' ', $line, "\n";
336
          $n++;
337
        }
338
        $result->free();
339
      }
340 1
341
      $continue = $this->mysqli->more_results();
342 1
      if ($continue)
343
      {
344 1
        $success = @$this->mysqli->next_result();
345
        if (!$success)
346
        {
347 1
          throw $this->dataLayerError('mysqli::next_result');
348 1
        }
349 1
      }
350
    } while ($continue);
351 1
352 1
    return $n;
353
  }
354
355
  //--------------------------------------------------------------------------------------------------------------------
356 1
  /**
357
   * Executes multiple queries and returns an array with the "result" of each query, i.e. the length of the returned
358
   * array equals the number of queries. For SELECT, SHOW, DESCRIBE or EXPLAIN queries the "result" is the selected
359 1
   * rows (i.e. an array of arrays), for other queries the "result" is the number of effected rows.
360 1
   *
361
   * @param string $queries The SQL statements.
362 1
   *
363 1
   * @return array
364
   *
365
   * @throws MySqlDataLayerException
366
   *
367 1
   * @api
368
   * @since 1.0.0
369
   */
370
  public function executeMulti(string $queries): array
371
  {
372
    $ret = [];
373
374
    $this->multiQuery($queries);
375
    do
376
    {
377
      $result = $this->mysqli->store_result();
378
      if ($this->mysqli->errno)
379
      {
380
        throw $this->dataLayerError('mysqli::store_result');
381
      }
382
      if ($result)
383 119
      {
384
        $ret[] = $result->fetch_all(MYSQLI_ASSOC);
385 119
        $result->free();
386
      }
387 119
      else
388
      {
389 119
        $ret[] = $this->mysqli->affected_rows;
390
      }
391 119
392
      $continue = $this->mysqli->more_results();
393
      if ($continue)
394
      {
395
        $success = @$this->mysqli->next_result();
396
        if (!$success)
397
        {
398
          throw $this->dataLayerError('mysqli::next_result');
399
        }
400
      }
401
    } while ($continue);
402
403
    return $ret;
404
  }
405
406
  //--------------------------------------------------------------------------------------------------------------------
407
  /**
408
   * Executes a query that does not select any rows.
409 6
   *
410
   * @param string $query The SQL statement.
411 6
   *
412 6
   * @return int The number of affected rows (if any).
413 6
   *
414 6
   * @throws MySqlQueryErrorException
415
   *
416 6
   * @api
417
   * @since 1.0.0
418 6
   */
419
  public function executeNone(string $query): int
420 1
  {
421
    $this->realQuery($query);
422
423 5
    $n = $this->mysqli->affected_rows;
424
425
    if ($this->mysqli->more_results())
426
    {
427
      $this->mysqli->next_result();
428
    }
429
430
    return $n;
431
  }
432
433
  //--------------------------------------------------------------------------------------------------------------------
434
  /**
435
   * Executes a query that returns 0 or 1 row.
436
   * Throws an exception if the query selects 2 or more rows.
437
   *
438
   * @param string $query The SQL statement.
439
   *
440
   * @return array|null The selected row.
441 43
   *
442
   * @throws MySqlQueryErrorException
443 43
   * @throws ResultException
444 43
   *
445 43
   * @api
446 43
   * @since 1.0.0
447
   */
448 43
  public function executeRow0(string $query): ?array
449
  {
450 43
    $result = $this->query($query);
451
    $row    = $result->fetch_assoc();
452 2
    $n      = $result->num_rows;
453
    $result->free();
454
455 41
    if ($this->mysqli->more_results())
456
    {
457
      $this->mysqli->next_result();
458
    }
459
460
    if (!($n==0 || $n==1))
461
    {
462
      throw new ResultException([0, 1], $n, $query);
463
    }
464
465
    return $row;
466
  }
467
468
  //--------------------------------------------------------------------------------------------------------------------
469
  /**
470
   * Executes a query that returns 1 and only 1 row.
471 6
   * Throws an exception if the query selects none, 2 or more rows.
472
   *
473 6
   * @param string $query The SQL statement.
474 5
   *
475 5
   * @return array The selected row.
476
   *
477 5
   * @throws MySqlQueryErrorException
478
   * @throws ResultException
479 5
   *
480
   * @api
481
   * @since 1.0.0
482
   */
483
  public function executeRow1(string $query): array
484
  {
485
    $result = $this->query($query);
486
    $row    = $result->fetch_assoc();
487
    $n      = $result->num_rows;
488
    $result->free();
489
490
    if ($this->mysqli->more_results())
491
    {
492
      $this->mysqli->next_result();
493
    }
494
495
    if ($n!=1)
496
    {
497 18
      throw new ResultException([1], $n, $query);
498
    }
499 18
500 18
    return $row;
501 18
  }
502 18
503
  //--------------------------------------------------------------------------------------------------------------------
504 18
  /**
505
   * Executes a query that returns 0 or more rows.
506 18
   *
507
   * @param string $query The SQL statement.
508 3
   *
509
   * @return array[] The selected rows.
510
   *
511 15
   * @throws MySqlQueryErrorException
512
   *
513 13
   * @api
514
   * @since 1.0.0
515
   */
516 2
  public function executeRows(string $query): array
517
  {
518
    $result = $this->query($query);
519
    $rows   = $result->fetch_all(MYSQLI_ASSOC);
520
    $result->free();
521
522
    if ($this->mysqli->more_results())
523
    {
524
      $this->mysqli->next_result();
525
    }
526
527
    return $rows;
528
  }
529
530
  //--------------------------------------------------------------------------------------------------------------------
531
  /**
532
   * Executes a query that returns 0 or 1 row with one column.
533
   * Throws an exception if the query selects 2 or more rows.
534 16
   *
535
   * @param string $query The SQL statement.
536 16
   *
537 16
   * @return mixed The selected value.
538 16
   *
539 16
   * @throws MySqlQueryErrorException
540
   * @throws ResultException
541 16
   *
542
   * @api
543 16
   * @since 1.0.0
544
   */
545 4
  public function executeSingleton0(string $query): mixed
546
  {
547
    $result = $this->query($query);
548 12
    $row    = $result->fetch_array(MYSQLI_NUM);
549
    $n      = $result->num_rows;
550
    $result->free();
551
552
    if ($this->mysqli->more_results())
553
    {
554
      $this->mysqli->next_result();
555
    }
556
557
    if ($n==0)
558
    {
559
      return null;
560
    }
561
562
    if ($n==1)
563
    {
564
      return $row[0];
565 1
    }
566
567 1
    throw new ResultException([0, 1], $n, $query);
568
  }
569 1
570
  //--------------------------------------------------------------------------------------------------------------------
571
  /**
572 1
   * Executes a query that returns 1 and only 1 row with 1 column.
573 1
   * Throws an exception if the query selects none, 2 or more rows.
574 1
   *
575
   * @param string $query The SQL statement.
576 1
   *
577
   * @return mixed The selected value.
578
   *
579 1
   * @throws MySqlQueryErrorException
580
   * @throws ResultException
581 1
   *
582 1
   * @api
583 1
   * @since 1.0.0
584
   */
585
  public function executeSingleton1(string $query): mixed
586
  {
587 1
    $result = $this->query($query);
588
    $row    = $result->fetch_array(MYSQLI_NUM);
589
    $n      = $result->num_rows;
590 1
    $result->free();
591
592 1
    if ($this->mysqli->more_results())
593
    {
594
      $this->mysqli->next_result();
595 1
    }
596
597 1
    if ($n!=1)
598
    {
599 1
      throw new ResultException([1], $n, $query);
600 1
    }
601
602
    return $row[0];
603 1
  }
604
605
  //--------------------------------------------------------------------------------------------------------------------
606
  /**
607 1
   * Executes a query and shows the data in formatted in a table (like mysql's default pager) of in multiple tables
608
   * (in case of a multi query).
609
   *
610 1
   * @param string $query The query.
611 1
   *
612
   * @return int The total number of rows in the tables.
613 1
   *
614 1
   * @throws MySqlDataLayerException
615
   *
616
   * @api
617
   * @since 1.0.0
618 1
   */
619
  public function executeTable(string $query): int
620
  {
621
    $rowCount = 0;
622
623
    $this->multiQuery($query);
624
    do
625
    {
626
      $result = @$this->mysqli->store_result();
627
      if ($this->mysqli->errno)
628
      {
629
        throw $this->dataLayerError('mysqli::store_result');
630 38
      }
631
      if ($result)
632 38
      {
633
        $columns = [];
634 38
635 38
        // Get metadata to array.
636
        foreach ($result->fetch_fields() as $key => $column)
637 38
        {
638
          $columns[$key]['header'] = $column->name;
639
          $columns[$key]['type']   = $column->type;
640
          switch ($column->type)
641
          {
642
            case 12:
643 38
              $length = 19;
644
              break;
645
646 38
            default:
647
              $length = $column->max_length;
648
          }
649
          $columns[$key]['length'] = max(4, $length, mb_strlen($column->name));
650
        }
651
652
        // Show the table header.
653
        $this->executeTableShowHeader($columns);
654
655
        // Show for all rows all columns.
656
        while (($row = $result->fetch_row()))
657
        {
658
          $rowCount++;
659
660
          // First row separator.
661
          echo '|';
662
663
          foreach ($row as $i => $value)
664
          {
665
            $this->executeTableShowTableColumn($columns[$i], $value);
666
            echo '|';
667
          }
668
669
          echo "\n";
670
        }
671
672
        // Show the table footer.
673
        $this->executeTableShowFooter($columns);
674
      }
675
676
      $continue = $this->mysqli->more_results();
677
      if ($continue)
678
      {
679
        $result = @$this->mysqli->next_result();
680
        if (!$result)
681
        {
682
          throw $this->dataLayerError('mysqli::next_result');
683
        }
684
      }
685
    } while ($continue);
686
687
    return $rowCount;
688
  }
689 5
690
  //--------------------------------------------------------------------------------------------------------------------
691 5
  /**
692
   * Returns the value of the MySQL variable max_allowed_packet.
693 3
   *
694
   * @return int
695
   *
696
   * @throws MySqlQueryErrorException
697
   * @throws ResultException
698
   */
699
  public function getMaxAllowedPacket(): int
700
  {
701
    if ($this->maxAllowedPacket===null)
702
    {
703
      $query            = "show variables like 'max_allowed_packet'";
704 5
      $maxAllowedPacket = $this->executeRow1($query);
705
706 5
      $this->maxAllowedPacket = (int)$maxAllowedPacket['Value'];
707
708 4
      // Note: When setting $chunkSize equal to $maxAllowedPacket it is not possible to transmit a LOB
709
      // with size $maxAllowedPacket bytes (but only $maxAllowedPacket - 8 bytes). But when setting the size of
710
      // $chunkSize less than $maxAllowedPacket than it is possible to transmit a LOB with size
711 5
      // $maxAllowedPacket bytes.
712
      $this->chunkSize = (int)min($this->maxAllowedPacket - 8, 1024 * 1024);
713
    }
714
715
    return $this->maxAllowedPacket;
716
  }
717
718
  //--------------------------------------------------------------------------------------------------------------------
719
  /**
720
   * Returns the query log.
721
   *
722 5
   * To enable the query log set {@link $queryLog} to true.
723
   *
724 5
   * @return array[]
725
   *
726 5
   * @since 1.0.0
727
   * @api
728 5
   */
729
  public function getQueryLog(): array
730
  {
731
    return $this->queryLog;
732
  }
733
734
  //--------------------------------------------------------------------------------------------------------------------
735
  /**
736
   * Returns whether PHP is (still) connected to a MySQL or MariaDB instance.
737
   *
738
   * This method will never throw an exception.
739 6
   *
740
   * @return bool
741 6
   *
742
   * @since 5.0.0
743 4
   * @api
744
   */
745
  public function isAlive(): bool
746
  {
747
    return $this->connector->isAlive();
748
  }
749
750
  //--------------------------------------------------------------------------------------------------------------------
751
  /**
752
   * Returns a hexadecimal literal for a binary value that can be safely used in SQL statements.
753
   *
754 81
   * @param string|null $value The binary value.
755
   *
756 81
   * @return string
757
   */
758 78
  public function quoteBinary(?string $value): string
759
  {
760
    if ($value===null || $value==='')
761
    {
762
      return 'null';
763
    }
764
765
    return '0x'.bin2hex($value);
766
  }
767
768
  //--------------------------------------------------------------------------------------------------------------------
769
  /**
770
   * Returns a literal for a bit value that can be safely used in SQL statements.
771
   *
772
   * @param string|null $bits The bit value.
773
   *
774
   * @return string
775 13
   */
776
  public function quoteBit(?string $bits): string
777 13
  {
778
    if ($bits===null || $bits==='')
779 2
    {
780
      return 'null';
781
    }
782 11
783 11
    return "b'".$this->mysqli->real_escape_string($bits)."'";
784
  }
785 5
786
  //--------------------------------------------------------------------------------------------------------------------
787 6
  /**
788
   * Returns a literal for a decimal value that can be safely used in SQL statements.
789
   *
790
   * @param int|float|string|null $value The value.
791
   *
792 11
   * @return string
793
   */
794 11
  public function quoteDecimal(int|float|string|null $value): string
795
  {
796
    if ($value===null || $value==='')
797
    {
798 11
      return 'null';
799
    }
800 9
801
    if (is_int($value) || is_float($value))
802
    {
803 7
      return (string)$value;
804 7
    }
805
806
    return "'".$this->mysqli->real_escape_string($value)."'";
807 2
  }
808
809
  //--------------------------------------------------------------------------------------------------------------------
810
  /**
811
   * Returns a literal for a float value that can be safely used in SQL statements.
812
   *
813
   * @param float|null $value The float value.
814
   *
815
   * @return string
816
   */
817
  public function quoteFloat(?float $value): string
818 15
  {
819
    if ($value===null)
820 15
    {
821
      return 'null';
822 13
    }
823
824
    return (string)$value;
825
  }
826
827
  //--------------------------------------------------------------------------------------------------------------------
828
  /**
829
   * Returns a literal for an integer value that can be safely used in SQL statements.
830
   *
831
   * @param int|null $value The integer value.
832
   *
833
   * @return string
834
   */
835 1
  public function quoteInt(?int $value): string
836
  {
837 1
    if ($value===null)
838
    {
839
      return 'null';
840
    }
841
842
    return (string)$value;
843
  }
844
845
  //--------------------------------------------------------------------------------------------------------------------
846
  /**
847
   * Returns a literal for an expression with a separated list of integers that can be safely used in SQL
848
   * statements. Throws an exception if the value is a list of integers.
849
   *
850
   * @param array|string|null $list      The list of integers.
851
   * @param string            $delimiter The field delimiter (one character only).
852
   * @param string            $enclosure The field enclosure character (one character only).
853
   * @param string            $escape    The escape character (one character only)
854
   *
855
   * @return string
856
   *
857
   * @throws LogicException
858
   */
859
  public function quoteListOfInt(array|string|null $list, string $delimiter, string $enclosure, string $escape): string
860
  {
861
    if ($list===null || $list==='' || $list===[])
0 ignored issues
show
introduced by
The condition $list === '' is always false.
Loading history...
862
    {
863
      return 'null';
864 1
    }
865
866 1
    if (is_string($list))
0 ignored issues
show
introduced by
The condition is_string($list) is always false.
Loading history...
867
    {
868 1
      $list = str_getcsv($list, $delimiter, $enclosure, $escape);
869
    }
870
871 1
    foreach ($list as $number)
872
    {
873
      if (!is_numeric($number))
874
      {
875
        throw new LogicException("Value '%s' is not a number.", (is_scalar($number)) ? $number : gettype($number));
876
      }
877
    }
878
879
    return $this->quoteString(implode(',', $list));
880
  }
881
882
  //--------------------------------------------------------------------------------------------------------------------
883
  /**
884
   * Returns a literal for a string value that can be safely used in SQL statements.
885
   *
886
   * @param string|null $value The value.
887
   *
888
   * @return string
889
   */
890
  public function quoteString(?string $value): string
891
  {
892
    if ($value===null || $value==='')
893
    {
894
      return 'null';
895
    }
896
897
    return "'".$this->mysqli->real_escape_string($value)."'";
898 1
  }
899
900 1
  //--------------------------------------------------------------------------------------------------------------------
901
  /**
902
   * Escapes special characters in a string such that it can be safely used in SQL statements.
903
   *
904
   * MysqlWrapper around [mysqli::real_escape_string](http://php.net/manual/mysqli.real-escape-string.php).
905
   *
906
   * @param string $string The string.
907
   *
908
   * @return string
909
   */
910
  public function realEscapeString(string $string): string
911
  {
912
    return $this->mysqli->real_escape_string($string);
913
  }
914 3
915
  //--------------------------------------------------------------------------------------------------------------------
916 3
  /**
917
   * Rollbacks the current transaction (and starts a new transaction).
918
   *
919
   * MysqlWrapper around [mysqli::rollback](http://php.net/manual/en/mysqli.rollback.php), however on failure an
920
   * exception is thrown.
921
   *
922
   * @throws MySqlDataLayerException
923
   *
924
   * @api
925
   * @since 1.0.0
926
   */
927 3
  public function rollback(): void
928 3
  {
929
    $success = @$this->mysqli->rollback();
930
    if (!$success)
931
    {
932
      throw $this->dataLayerError('mysqli::rollback');
933
    }
934
  }
935
936
  //--------------------------------------------------------------------------------------------------------------------
937
  /**
938
   * Sets the connector of this data layer. The data layer must be disconnected from the MySQL instance.
939
   *
940
   * @param MySqlConnector $connector The new connector.
941
   */
942
  public function setConnector(MySqlConnector $connector): void
943
  {
944
    if ($this->mysqli!==null)
945
    {
946 97
      throw new \LogicException('Can not set connector of a connected data layer. Disconnect first.');
947
    }
948 97
949
    $this->connector = $connector;
950
  }
951
952
  //--------------------------------------------------------------------------------------------------------------------
953
  /**
954
   * Logs the warnings of the last executed SQL statement.
955
   *
956
   * MysqlWrapper around the SQL statement [show warnings](https://dev.mysql.com/doc/refman/5.6/en/show-warnings.html).
957
   *
958
   * @throws MySqlDataLayerException
959
   *
960
   * @api
961
   * @since 1.0.0
962 97
   */
963 97
  public function showWarnings(): void
964
  {
965 1
    $this->executeLog('show warnings');
966
  }
967
968
  //--------------------------------------------------------------------------------------------------------------------
969 96
  /**
970
   * Return an exception with error information provided by MySQL/[mysqli](http://php.net/manual/en/class.mysqli.php).
971
   *
972
   * @param string $method The name of the method that has failed.
973
   *
974
   * @return MySqlDataLayerException
975
   */
976
  protected function dataLayerError(string $method): MySqlDataLayerException
977
  {
978
    return new MySqlDataLayerException($this->mysqli->errno, $this->mysqli->error, $method);
979
  }
980
981 4
  //--------------------------------------------------------------------------------------------------------------------
982
  /**
983 4
   * Executes multiple SQL statements.
984
   *
985
   * MysqlWrapper around [multi_mysqli::query](http://php.net/manual/mysqli.multi-query.php), however on failure an
986
   * exception is thrown.
987
   *
988
   * @param string $queries The SQL statements.
989
   *
990
   * @throws MySqlQueryErrorException
991
   */
992
  protected function multiQuery(string $queries): void
993
  {
994
    if ($this->logQueries)
995
    {
996
      $time0 = microtime(true);
997
    }
998
999 119
    try
1000
    {
1001 119
      $ret = @$this->mysqli->multi_query($queries);
1002
    }
1003
    catch (\mysqli_sql_exception)
1004
    {
1005
      $ret = false;
1006
    }
1007
    if ($ret===false)
1008
    {
1009
      throw $this->queryError('mysqli::multi_query', $queries);
1010
    }
1011
1012
    if ($this->logQueries)
1013 119
    {
1014 119
      $this->queryLog[] = ['query' => $queries, 'time' => microtime(true) - $time0];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $time0 does not seem to be defined for all execution paths leading up to this point.
Loading history...
1015
    }
1016
  }
1017
1018
  //--------------------------------------------------------------------------------------------------------------------
1019
  /**
1020
   * Executes a query (i.e. SELECT, SHOW, DESCRIBE or EXPLAIN) with a result set.
1021
   *
1022
   * MysqlWrapper around [mysqli::query](http://php.net/manual/mysqli.query.php), however on failure an exception is
1023
   * thrown.
1024
   *
1025
   * For other SQL statements, see @realQuery.
1026
   *
1027
   * @param string $query The SQL statement.
1028
   *
1029
   * @return \mysqli_result
1030 38
   *
1031
   * @throws MySqlQueryErrorException
1032 38
   */
1033
  protected function query(string $query): \mysqli_result
1034 37
  {
1035 37
    if ($this->logQueries)
1036 37
    {
1037
      $time0 = microtime(true);
1038 37
    }
1039 37
1040 37
    try
1041
    {
1042
      $ret = @$this->mysqli->query($query);
1043
    }
1044
    catch (\mysqli_sql_exception)
1045
    {
1046
      $ret = false;
1047
    }
1048
    if ($ret===false)
1049
    {
1050
      throw $this->queryError('mysqli::query', $query);
1051 1
    }
1052
1053 1
    if ($this->logQueries)
1054
    {
1055 1
      $this->queryLog[] = ['query' => $query, 'time' => microtime(true) - $time0];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $time0 does not seem to be defined for all execution paths leading up to this point.
Loading history...
1056
    }
1057 1
1058
    return $ret;
1059 1
  }
1060
1061
  //--------------------------------------------------------------------------------------------------------------------
1062
  /**
1063
   * Throws an exception with error information provided by MySQL/[mysqli](http://php.net/manual/en/class.mysqli.php).
1064
   *
1065
   * @param string $method The name of the method that has failed.
1066
   * @param string $query  The failed query.
1067
   *
1068 1
   * @return MySqlQueryErrorException
1069
   */
1070 1
  protected function queryError(string $method, string $query): MySqlQueryErrorException
1071 1
  {
1072
    return new MySqlQueryErrorException($this->mysqli->errno, $this->mysqli->error, $method, $query);
1073 1
  }
1074
1075 1
  //--------------------------------------------------------------------------------------------------------------------
1076 1
  /**
1077
   * Execute a query without a result set.
1078 1
   *
1079 1
   * MysqlWrapper around [mysqli::real_query](http://php.net/manual/en/mysqli.real-query.php), however on failure an
1080
   * exception is thrown.
1081 1
   *
1082 1
   * For SELECT, SHOW, DESCRIBE or EXPLAIN queries, see @query.
1083
   *
1084 1
   * @param string $query The SQL statement.
1085
   *
1086
   * @throws MySqlQueryErrorException
1087 1
   */
1088 1
  protected function realQuery(string $query): void
1089 1
  {
1090
    if ($this->logQueries)
1091
    {
1092
      $time0 = microtime(true);
1093
    }
1094
1095
    try
1096
    {
1097
      $success = @$this->mysqli->real_query($query);
1098
    }
1099 1
    catch (\mysqli_sql_exception)
1100
    {
1101 1
      $success = false;
1102
    }
1103 1
    if (!$success)
1104
    {
1105 1
      throw $this->queryError('mysqli::real_query', $query);
1106 1
    }
1107 1
1108 1
    if ($this->logQueries)
1109 1
    {
1110 1
      $this->queryLog[] = ['query' => $query,
1111 1
                           'time'  => microtime(true) - $time0];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $time0 does not seem to be defined for all execution paths leading up to this point.
Loading history...
1112 1
    }
1113 1
  }
1114 1
1115
  //--------------------------------------------------------------------------------------------------------------------
1116 1
  /**
1117 1
   * Send data in blocks to the MySQL server.
1118 1
   *
1119 1
   * MysqlWrapper around [mysqli_stmt::send_long_data](http://php.net/manual/mysqli-stmt.send-long-data.php).
1120 1
   *
1121 1
   * @param \mysqli_stmt $statement The prepared statement.
1122 1
   * @param int          $paramNr   The 0-indexed parameter number.
1123 1
   * @param string|null  $data      The data.
1124
   *
1125 1
   * @throws MySqlDataLayerException
1126 1
   */
1127
  protected function sendLongData(\mysqli_stmt $statement, int $paramNr, ?string $data): void
1128
  {
1129
    if ($data!==null)
1130
    {
1131
      $n = strlen($data);
1132
      $p = 0;
1133
      while ($p<$n)
1134
      {
1135
        $success = @$statement->send_long_data($paramNr, substr($data, $p, $this->chunkSize));
1136
        if (!$success)
1137
        {
1138
          throw $this->dataLayerError('mysqli_stmt::send_long_data');
1139
        }
1140
        $p += $this->chunkSize;
1141
      }
1142
    }
1143
  }
1144
1145
  //--------------------------------------------------------------------------------------------------------------------
1146
  /**
1147
   * Helper method for method executeTable. Shows table footer.
1148
   *
1149
   * @param array $columns
1150
   */
1151
  private function executeTableShowFooter(array $columns): void
1152
  {
1153
    $separator = '+';
1154
1155
    foreach ($columns as $column)
1156
    {
1157
      $separator .= str_repeat('-', $column['length'] + 2).'+';
1158
    }
1159
    echo $separator, "\n";
1160
  }
1161
1162
  //--------------------------------------------------------------------------------------------------------------------
1163
  /**
1164
   * Helper method for method executeTable. Shows table header.
1165
   *
1166
   * @param array $columns
1167
   */
1168
  private function executeTableShowHeader(array $columns): void
1169
  {
1170
    $separator = '+';
1171
    $header    = '|';
1172
1173
    foreach ($columns as $column)
1174
    {
1175
      $separator .= str_repeat('-', $column['length'] + 2).'+';
1176
      $spaces    = ($column['length'] + 2) - mb_strlen((string)$column['header']);
1177
1178
      $spacesLeft  = (int)floor($spaces / 2);
1179
      $spacesRight = (int)ceil($spaces / 2);
1180
1181
      $fillerLeft  = ($spacesLeft>0) ? str_repeat(' ', $spacesLeft) : '';
1182
      $fillerRight = ($spacesRight>0) ? str_repeat(' ', $spacesRight) : '';
1183
1184
      $header .= $fillerLeft.$column['header'].$fillerRight.'|';
1185
    }
1186
1187
    echo "\n", $separator, "\n";
1188
    echo $header, "\n";
1189
    echo $separator, "\n";
1190
  }
1191
1192
  //--------------------------------------------------------------------------------------------------------------------
1193
  /**
1194
   * Helper method for method executeTable. Shows table cell with data.
1195
   *
1196
   * @param array $column The metadata of the column.
1197
   * @param mixed $value  The value of the table cell.
1198
   */
1199
  private function executeTableShowTableColumn(array $column, mixed $value): void
1200
  {
1201
    $spaces = str_repeat(' ', max($column['length'] - mb_strlen((string)$value), 0));
1202
1203
    switch ($column['type'])
1204
    {
1205
      case 1: // tinyint
1206
      case 2: // smallint
1207
      case 3: // int
1208
      case 4: // float
1209
      case 5: // double
1210
      case 8: // bigint
1211
      case 9: // mediumint
1212
      case 246: // decimal
1213
        echo ' ', $spaces.$value, ' ';
1214
        break;
1215
1216
      case 7: // timestamp
1217
      case 10: // date
1218
      case 11: // time
1219
      case 12: // datetime
1220
      case 13: // year
1221
      case 16: // bit
1222
      case 252: // is currently mapped to all text and blob types (MySQL 5.0.51a)
1223
      case 253: // varchar
1224
      case 254: // char
1225
        echo ' ', $value.$spaces, ' ';
1226
        break;
1227
1228
      default:
1229
        throw new FallenException('data type id', $column['type']);
1230
    }
1231
  }
1232
1233
  //--------------------------------------------------------------------------------------------------------------------
1234
}
1235
1236
//----------------------------------------------------------------------------------------------------------------------
1237