Completed
Push — master ( 16ba6c...34b4d5 )
by Agel_Nash
03:40
created

Database::save()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 7
nop 3
dl 0
loc 18
ccs 0
cts 11
cp 0
crap 30
rs 8.8571
c 0
b 0
f 0
1
<?php namespace AgelxNash\Modx\Evo\Database;
2
3
use AgelxNash\Modx\Evo\Database\Exceptions\QueryException;
4
use mysqli;
5
use mysqli_result;
6
use mysqli_sql_exception;
7
use mysqli_driver;
8
9
class Database implements DatabaseInterface
10
{
11
    use Traits\DebugTrait,
12
        Traits\SupportTrait;
13
14
    protected const SAFE_LOOP_COUNT = 1000;
15
16
    /**
17
     * @var mysqli
18
     */
19
    public $conn;
20
    public $config = [];
21
22
23
    /**
24
     * @param string $host
25
     * @param string $base
26
     * @param string $user
27
     * @param string $pass
28
     * @param string $prefix
29
     * @param string $charset
30
     * @param string $method
31
     */
32 14
    public function __construct(
33
        $host = '',
34
        $base = '',
35
        $user = '',
36
        $pass = '',
37
        $prefix = '',
38
        $charset = 'utf8mb4',
39
        $method = 'SET CHARACTER SET'
40
    ) {
41 14
        $base = trim($base, '`');
42
43 14
        $driver = new mysqli_driver();
44 14
        $driver->report_mode = MYSQLI_REPORT_STRICT | MYSQLI_REPORT_ERROR;
45
46 14
        $this->setConfig(compact(
47 14
            'host',
48 14
            'base',
49 14
            'user',
50 14
            'pass',
51 14
            'prefix',
52 14
            'charset',
53 14
            'method'
54
        ));
55 14
    }
56
57
    /**
58
     * @param $data
59
     * @return $this
60
     */
61 14
    public function setConfig($data) : self
62
    {
63 14
        $this->config = $data;
64
65 14
        return $this;
66
    }
67
68
    /**
69
     * @param null|string $key
70
     * @return mixed
71
     */
72 2
    public function getConfig($key = null)
73
    {
74 2
        return ($key === null ? $this->config : ($this->config[$key] ?? null));
75
    }
76
77
    /**
78
     * @return mysqli
79
     * @throws Exceptions\ConnectException
80
     * @throws Exceptions\QueryException
81
     */
82
    public function getConnect() : mysqli
83
    {
84
        if (! $this->isConnected()) {
85
            return $this->connect();
86
        }
87
88
        return $this->conn;
89
    }
90
91
    /**
92
     * @return mysqli
93
     * @throws Exceptions\ConnectException
94
     * @throws Exceptions\QueryException
95
     */
96
    public function connect() : mysqli
97
    {
98
        $tStart = microtime(true);
99
        try {
100
            $this->conn = new mysqli(
101
                $this->getConfig('host'),
102
                $this->getConfig('user'),
103
                $this->getConfig('pass'),
104
                $this->getConfig('base')
105
            );
106
107
            if ($this->isConnected() && $this->getConnect()->connect_error) {
108
                throw new Exceptions\ConnectException($this->conn->connect_error);
109
            }
110
111
            if (! $this->isConnected()) {
112
                throw new Exceptions\ConnectException(
113
                    $this->getLastError() ?: 'Failed to create the database connection!'
114
                );
115
            }
116
        } catch (mysqli_sql_exception $exception) {
117
            throw new Exceptions\ConnectException($exception->getMessage(), $exception->getCode());
118
        }
119
120
        $this->setCharset($this->getConfig('charset'), $this->getConfig('method'));
121
122
        $totalTime = microtime(true) - $tStart;
123
        if ($this->isDebug()) {
124
            $this->connectionTime = $totalTime;
125
        }
126
127
        return $this->conn;
128
    }
129
130
    /**
131
     * @return $this
132
     */
133
    public function disconnect() : self
134
    {
135
        if ($this->isConnected()) {
136
            $this->conn->close();
137
        }
138
        $this->conn = null;
139
        $this->connectionTime = 0;
140
        $this->flushExecutedQuery();
141
142
        return $this;
143
    }
144
145
    /**
146
     * @return bool
147
     */
148 1
    public function isConnected() : bool
149
    {
150 1
        return ($this->conn instanceof mysqli);
151
    }
152
153
    /**
154
     * @param string|array $data
155
     * @param int $safeCount
156
     * @return array|string
157
     * @throws Exceptions\ConnectException
158
     * @throws Exceptions\QueryException
159
     * @throws Exceptions\TooManyLoopsException
160
     */
161
    public function escape($data, $safeCount = 0)
162
    {
163
        $safeCount++;
164
        if (self::SAFE_LOOP_COUNT < $safeCount) {
165
            throw new Exceptions\TooManyLoopsException("Too many loops '{$safeCount}'");
166
        }
167
        if (\is_array($data)) {
168
            if (\count($data) === 0) {
169
                $data = '';
170
            } else {
171
                foreach ($data as $i => $v) {
172
                    $data[$i] = $this->escape($v, $safeCount);
173
                }
174
            }
175
        } else {
176
            $data = $this->getConnect()->escape_string($data);
177
        }
178
179
        return $data;
180
    }
181
182
    /**
183
     * @param mixed $sql
184
     * @return bool|mysqli_result
185
     * @throws Exceptions\ConnectException
186
     * @throws Exceptions\QueryException
187
     */
188
    public function query($sql)
189
    {
190
        $tStart = microtime(true);
191
        if (\is_array($sql)) {
192
            $sql = implode("\n", $sql);
193
        }
194
        $this->lastQuery = $sql;
195
196
        try {
197
            $result = $this->getConnect()->query($sql);
198
        } catch (mysqli_sql_exception $exception) {
199
            throw new Exceptions\QueryException($exception->getMessage(), $exception->getCode());
200
        }
201
202
        if ($result === false) {
203
            $this->checkLastError($sql);
204
        } else {
205
            $tend = microtime(true);
206
            $totalTime = $tend - $tStart;
207
            $this->queryTime += $totalTime;
208
            if ($this->isDebug()) {
209
                $this->collectQuery(
210
                    $result,
211
                    $sql,
212
                    $this->executedQueries + 1,
213
                    $totalTime
214
                );
215
            }
216
            $this->executedQueries++;
217
218
            return $result;
219
        }
220
        return false;
221
    }
222
223
    /**
224
     * @param string $table
225
     * @param array|string $where
226
     * @param string $orderBy
227
     * @param string $limit
228
     * @return bool|mysqli_result
229
     * @throws Exceptions\ConnectException
230
     * @throws Exceptions\QueryException
231
     * @throws Exceptions\TableNotDefinedException
232
     */
233
    public function delete($table, $where = '', $orderBy = '', $limit = '')
234
    {
235
        $table = $this->prepareFrom($table);
236
        $where = $this->prepareWhere($where);
237
        $orderBy = $this->prepareOrder($orderBy);
238
        $limit = $this->prepareOrder($limit);
239
240
        return $this->query("DELETE FROM {$table} {$where} {$orderBy} {$limit}");
241
    }
242
243
    /**
244
     * @param array|string $fields
245
     * @param array|string $tables
246
     * @param array|string $where
247
     * @param string $orderBy
248
     * @param string $limit
249
     * @return bool|mysqli_result
250
     * @throws Exceptions\ConnectException
251
     * @throws Exceptions\QueryException
252
     * @throws Exceptions\TableNotDefinedException
253
     */
254
    public function select($fields, $tables, $where = '', $orderBy = '', $limit = '')
255
    {
256
        $fields = $this->prepareFields($fields);
257
        $tables = $this->prepareFrom($tables, true);
258
        $where = $this->prepareWhere($where);
259
        $orderBy = $this->prepareOrder($orderBy);
260
        $limit = $this->prepareLimit($limit);
261
262
        return $this->query("SELECT {$fields} FROM {$tables} {$where} {$orderBy} {$limit}");
263
    }
264
265
    /**
266
     * @param array|string $values
267
     * @param string $table
268
     * @param array|string $where
269
     * @return bool|mysqli_result
270
     * @throws Exceptions\ConnectException
271
     * @throws Exceptions\QueryException
272
     * @throws Exceptions\TableNotDefinedException
273
     */
274
    public function update($values, string $table, $where = '')
275
    {
276
        $table = $this->prepareFrom($table);
277
        $values = $this->prepareValuesSet($values);
278
        $where = $this->prepareWhere($where);
279
280
        return $this->query("UPDATE {$table} SET {$values} {$where}");
281
    }
282
283
    /**
284
     * @param array|string $fields
285
     * @param string $table
286
     * @param string $fromfields
287
     * @param string $fromtable
288
     * @param array|string $where
289
     * @param string $limit
290
     * @return mixed
291
     * @throws Exceptions\ConnectException
292
     * @throws Exceptions\GetDataException
293
     * @throws Exceptions\QueryException
294
     * @throws Exceptions\TableNotDefinedException
295
     * @throws Exceptions\TooManyLoopsException
296
     */
297
    public function insert(
298
        $fields,
299
        string $table,
300
        string $fromfields = '*',
301
        string $fromtable = '',
302
        $where = '',
303
        string $limit = ''
304
    ) {
305
        $table = $this->prepareFrom($table);
306
307
        if (is_scalar($fields)) {
308
            $this->query("INSERT INTO {$table} {$fields}");
309
        } else {
310
            if (empty($fromtable) && \is_array($fields)) {
311
                $fields = $this->prepareValues($fields);
312
                $this->query("INSERT INTO {$table} {$fields}");
313
            } else {
314
                $fields = $this->prepareValues($fields, 2, true)['values'];
315
                $where = $this->prepareWhere($where);
316
                $limit = $this->prepareLimit($limit);
317
                $this->query("INSERT INTO {$table} {$fields} SELECT {$fromfields} FROM {$fromtable} {$where} {$limit}");
318
            }
319
        }
320
        if (($lid = $this->getInsertId()) === false) {
321
            throw new Exceptions\GetDataException("Couldn't get last insert key!");
322
        }
323
324
        return $lid;
325
    }
326
327
    /**
328
     * @param string|array $fields
329
     * @param string $table
330
     * @param string $where
331
     * @return bool|mixed|mysqli_result
332
     * @throws Exceptions\ConnectException
333
     * @throws Exceptions\GetDataException
334
     * @throws Exceptions\QueryException
335
     * @throws Exceptions\TableNotDefinedException
336
     * @throws Exceptions\TooManyLoopsException
337
     */
338
    public function save($fields, string $table, string $where = '')
339
    {
340
        if ($where === '') {
341
            $mode = 'insert';
342
        } else {
343
            $result = $this->select('*', $table, $where);
344
            if (! $result instanceof mysqli_result) {
345
                throw (new QueryException('Need mysqli_result'))
346
                    ->setQuery($this->getLastQuery());
347
            }
348
            if ($this->getRecordCount($result) === 0) {
349
                $mode = 'insert';
350
            } else {
351
                $mode = 'update';
352
            }
353
        }
354
355
        return ($mode === 'insert') ? $this->insert($fields, $table) : $this->update($fields, $table, $where);
356
    }
357
358
    /**
359
     * @param $result
360
     * @return bool
361
     */
362 1
    public function isResult($result) : bool
363
    {
364 1
        return $result instanceof mysqli_result;
365
    }
366
367
    /**
368
     * @param mysqli_result $result
369
     * @return $this
370
     */
371
    public function freeResult(mysqli_result $result) : self
372
    {
373
        $result->free_result();
374
375
        return $this;
376
    }
377
378
    /**
379
     * @param mysqli_result $result
380
     * @return int
381
     */
382
    public function numFields(mysqli_result $result) : int
383
    {
384
        return $result->field_count;
385
    }
386
387
    /**
388
     * @param mysqli_result $result
389
     * @param int $col
390
     * @return string|null
391
     */
392
    public function fieldName(mysqli_result $result, $col = 0) :? string
393
    {
394
        $field = $result->fetch_field_direct($col);
395
396
        return $field->name ?? null;
397
    }
398
399
    /**
400
     * @param string $charset
401
     * @param string|null $method
402
     * @return bool
403
     * @throws Exceptions\ConnectException
404
     * @throws QueryException
405
     */
406
    public function setCharset(string $charset, $method = null) : bool
407
    {
408
        if ($method !== null) {
409
            $this->query($method . ' ' . $charset);
410
        }
411
412
        $tStart = microtime(true);
413
414
        $result = $this->getConnect()->set_charset($charset);
415
416
        $this->queryTime += microtime(true) - $tStart;
417
418
        return $result;
419
    }
420
421
    /**
422
     * @param string $name
423
     * @return bool
424
     * @throws Exceptions\ConnectException
425
     * @throws Exceptions\QueryException
426
     */
427
    public function selectDb(string $name) : bool
428
    {
429
        $tStart = microtime(true);
430
431
        $result = $this->getConnect()->select_db($name);
432
433
        $this->queryTime += microtime(true) - $tStart;
434
435
        return $result;
436
    }
437
438
    /**
439
     * @param mysqli_result $result
440
     * @return int
441
     */
442
    public function getRecordCount(mysqli_result $result) : int
443
    {
444
        return $result->num_rows;
445
    }
446
447
    /**
448
     * @param mysqli_result $result
449
     * @param string $mode
450
     * @return array|mixed|object|\stdClass
451
     * @throws Exceptions\UnknownFetchTypeException
452
     */
453
    public function getRow(mysqli_result $result, $mode = 'assoc')
454
    {
455
        switch ($mode) {
456
            case 'assoc':
457
                $out = $result->fetch_assoc();
458
                break;
459
            case 'num':
460
                $out = $result->fetch_row();
461
                break;
462
            case 'object':
463
                $out = $result->fetch_object();
464
                break;
465
            case 'both':
466
                $out = $result->fetch_array(MYSQLI_BOTH);
467
                break;
468
            default:
469
                throw new Exceptions\UnknownFetchTypeException(
470
                    "Unknown get type ($mode) specified for fetchRow - must be empty, 'assoc', 'num' or 'both'."
471
                );
472
        }
473
474
        return $out;
475
    }
476
477
    /**
478
     * @param string string $name
479
     * @param mixed $result
480
     * @return array
481
     * @throws Exceptions\ConnectException
482
     * @throws Exceptions\QueryException
483
     * @throws Exceptions\UnknownFetchTypeException
484
     */
485
    public function getColumn(string $name, $result) : array
486
    {
487
        $col = [];
488
489
        if (! ($result instanceof mysqli_result)) {
490
            $result = $this->query($result);
491
        }
492
493
        if ($result instanceof mysqli_result) {
494
            while ($row = $this->getRow($result)) {
495
                $col[] = $row[$name];
496
            }
497
        }
498
499
        return $col;
500
    }
501
502
    /**
503
     * @param mixed $result
504
     * @return array
505
     * @throws Exceptions\ConnectException
506
     * @throws Exceptions\QueryException
507
     * @throws Exceptions\UnknownFetchTypeException
508
     */
509
    public function getColumnNames($result) : array
510
    {
511
        $names = [];
512
513
        if (! ($result instanceof mysqli_result)) {
514
            $result = $this->query($result);
515
        }
516
517
        if ($result instanceof mysqli_result) {
518
            $limit = $this->numFields($result);
519
            for ($i = 0; $i < $limit; $i++) {
520
                $names[] = $this->fieldName($result, $i);
521
            }
522
        }
523
524
        return $names;
525
    }
526
527
    /**
528
     * @param mixed $result
529
     * @return bool|mixed
530
     * @throws Exceptions\ConnectException
531
     * @throws Exceptions\QueryException
532
     * @throws Exceptions\UnknownFetchTypeException
533
     */
534
    public function getValue($result)
535
    {
536
        $out = false;
537
538
        if (! ($result instanceof mysqli_result)) {
539
            $result = $this->query($result);
540
        }
541
542
        if ($result instanceof mysqli_result) {
543
            $result = $this->getRow($result, 'num');
544
            $out = $result[0] ?? false;
545
        }
546
547
        return $out;
548
    }
549
550
    /**
551
     * @param string $table
552
     * @return array
553
     * @throws Exceptions\ConnectException
554
     * @throws Exceptions\QueryException
555
     * @throws Exceptions\UnknownFetchTypeException
556
     */
557
    public function getTableMetaData(string $table) : array
558
    {
559
        $metadata = [];
560
        if (! empty($table)) {
561
            $sql = 'SHOW FIELDS FROM ' . $table;
562
            $result = $this->query($sql);
563
            if ($result instanceof mysqli_result) {
564
                while ($row = $this->getRow($result)) {
565
                    $fieldName = $row['Field'];
566
                    $metadata[$fieldName] = $row;
567
                }
568
            }
569
        }
570
571
        return $metadata;
572
    }
573
574
    /**
575
     * @param mysqli_result $result
576
     * @param bool $index
577
     * @return array
578
     * @throws Exceptions\UnknownFetchTypeException
579
     */
580
    public function makeArray(mysqli_result $result, bool $index = false) : array
581
    {
582
        $rsArray = [];
583
        $iterator = 0;
584
        while ($row = $this->getRow($result)) {
585
            $returnIndex = $index !== false && isset($row[$index]) ? $row[$index] : $iterator;
586
            $rsArray[$returnIndex] = $row;
587
            $iterator++;
588
        }
589
590
        return $rsArray;
591
    }
592
593
    /**
594
     * @param mysqli_result $result
595
     * @param int $row
596
     * @return bool
597
     */
598
    public function dataSeek(mysqli_result $result, int $row) : bool
599
    {
600
        return $result->data_seek($row);
601
    }
602
603
    /**
604
     * @return string
605
     * @throws Exceptions\ConnectException
606
     * @throws Exceptions\QueryException
607
     */
608
    public function getVersion() : string
609
    {
610
        return $this->getConnect()->server_info;
611
    }
612
613
    /**
614
     * @param string $table
615
     * @return bool|mysqli_result
616
     * @throws Exceptions\ConnectException
617
     * @throws Exceptions\QueryException
618
     */
619
    public function optimize(string $table)
620
    {
621
        $result = $this->query('OPTIMIZE TABLE ' . $table);
622
        if ($result !== false) {
623
            $result = $this->query('ALTER TABLE ' . $table);
624
        }
625
626
        return $result;
627
    }
628
629
    /**
630
     * @param string $table
631
     * @return bool|mysqli_result
632
     * @throws Exceptions\ConnectException
633
     * @throws Exceptions\QueryException
634
     */
635
    public function truncate(string $table)
636
    {
637
        return $this->query('TRUNCATE ' . $table);
638
    }
639
640
    /**
641
     * @return mixed
642
     * @throws Exceptions\ConnectException
643
     * @throws Exceptions\QueryException
644
     */
645
    public function getInsertId()
646
    {
647
        return $this->getConnect()->insert_id;
648
    }
649
650
    /**
651
     * @return int
652
     * @throws Exceptions\ConnectException
653
     * @throws Exceptions\QueryException
654
     */
655
    public function getAffectedRows() : int
656
    {
657
        return $this->getConnect()->affected_rows;
658
    }
659
}
660