Passed
Push — master ( 151185...42f330 )
by P.R.
04:09
created

MySqlDataLayer::executeLog()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 9

Importance

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