Passed
Pull Request — master (#684)
by Def
06:06 queued 03:45
created

AbstractCommand::logQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Command;
6
7
use Closure;
8
use Throwable;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Expression\Expression;
11
use Yiisoft\Db\Query\Data\DataReaderInterface;
12
use Yiisoft\Db\Query\QueryInterface;
13
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
14
15
use function current;
16
use function explode;
17
use function get_resource_type;
18
use function is_array;
19
use function is_bool;
20
use function is_int;
21
use function is_object;
22
use function is_resource;
23
use function is_scalar;
24
use function is_string;
25
use function preg_replace_callback;
26
use function stream_get_contents;
27
use function strncmp;
28
29
/**
30
 * Represents an SQL statement to execute in a database.
31
 *
32
 * It's usually created by calling {@see \Yiisoft\Db\Connection\ConnectionInterface::createCommand()}.
33
 *
34
 * You can get the SQL statement it represents via the {@see getSql()} method.
35
 *
36
 * To execute a non-query SQL (such as `INSERT`, `DELETE`, `UPDATE`), call {@see execute()}.
37
 *
38
 * To execute a SQL statement that returns a result (such as `SELECT`), use {@see queryAll()}, {@see queryOne()},
39
 * {@see queryColumn()}, {@see queryScalar()}, or {@see query()}.
40
 *
41
 * For example,
42
 *
43
 * ```php
44
 * $users = $connectionInterface->createCommand('SELECT * FROM user')->queryAll();
45
 * ```
46
 *
47
 * Abstract command supports SQL prepared statements and parameter binding.
48
 *
49
 * Call {@see bindValue()} to bind a value to a SQL parameter.
50
 * Call {@see bindParam()} to bind a PHP variable to a SQL parameter.
51
 *
52
 * When binding a parameter, the SQL statement is automatically prepared. You may also call {@see prepare()} explicitly
53
 * to do it.
54
 *
55
 * Abstract command supports building some SQL statements using methods such as {@see insert()}, {@see update()}, {@see delete()},
56
 * etc.
57
 *
58
 * For example, the following code will create and execute an `INSERT` SQL statement:
59
 *
60
 * ```php
61
 * $connectionInterface->createCommand()->insert(
62
 *     'user',
63
 *     ['name' => 'Sam', 'age' => 30],
64
 * )->execute();
65
 * ```
66
 *
67
 * To build `SELECT` SQL statements, please use {@see QueryInterface} and its implementations instead.
68
 */
69
abstract class AbstractCommand implements CommandInterface
70
{
71
    /**
72
     * Command in this query mode returns count of affected rows.
73
     *
74
     * @see execute()
75
     */
76
    protected const QUERY_MODE_EXECUTE = 1;
77
    /**
78
     * Command in this query mode returns the first row of selected data.
79
     *
80
     * @see queryOne()
81
     */
82
    protected const QUERY_MODE_ROW = 2;
83
    /**
84
     * Command in this query mode returns all rows of selected data.
85
     *
86
     * @see queryAll()
87
     */
88
    protected const QUERY_MODE_ALL = 4;
89
    /**
90
     * Command in this query mode returns all rows with the first column of selected data.
91
     *
92
     * @see queryColumn()
93
     */
94
    protected const QUERY_MODE_COLUMN = 8;
95
    /**
96
     * Command in this query mode returns {@see DataReaderInterface}, an abstraction for database cursor for
97
     * selected data.
98
     *
99
     * @see query()
100
     */
101
    protected const QUERY_MODE_CURSOR = 16;
102
    /**
103
     * Command in this query mode returns the first column in the first row of the query result
104
     * The value
105
     *
106
     * @see queryScalar()
107
     */
108
    protected const QUERY_MODE_SCALAR = 32;
109
110
    /**
111
     * @var string|null Transaction isolation level.
112
     */
113
    protected string|null $isolationLevel = null;
114
    /**
115
     * @var array Parameters to use.
116
     *
117
     * @psalm-var ParamInterface[]
118
     */
119
    protected array $params = [];
120
    /**
121
     * @var string|null Name of the table to refresh schema for. Null means not to refresh the schema.
122
     */
123
    protected string|null $refreshTableName = null;
124
    protected Closure|null $retryHandler = null;
125
    /**
126
     * @var string The SQL statement to execute.
127
     */
128
    private string $sql = '';
129
130
    public function addCheck(string $table, string $name, string $expression): static
131
    {
132
        $sql = $this->getQueryBuilder()->addCheck($table, $name, $expression);
133
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
134
    }
135
136
    public function addColumn(string $table, string $column, string $type): static
137
    {
138
        $sql = $this->getQueryBuilder()->addColumn($table, $column, $type);
139
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
140
    }
141
142
    public function addCommentOnColumn(string $table, string $column, string $comment): static
143
    {
144
        $sql = $this->getQueryBuilder()->addCommentOnColumn($table, $column, $comment);
145
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
146
    }
147
148
    public function addCommentOnTable(string $table, string $comment): static
149
    {
150
        $sql = $this->getQueryBuilder()->addCommentOnTable($table, $comment);
151
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
152
    }
153
154
    public function addDefaultValue(string $table, string $name, string $column, mixed $value): static
155
    {
156
        $sql = $this->getQueryBuilder()->addDefaultValue($table, $name, $column, $value);
157
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
158
    }
159
160
    public function addForeignKey(
161
        string $table,
162
        string $name,
163
        array|string $columns,
164
        string $referenceTable,
165
        array|string $referenceColumns,
166
        string $delete = null,
167
        string $update = null
168
    ): static {
169
        $sql = $this->getQueryBuilder()->addForeignKey(
170
            $table,
171
            $name,
172
            $columns,
173
            $referenceTable,
174
            $referenceColumns,
175
            $delete,
176
            $update
177
        );
178
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
179
    }
180
181
    public function addPrimaryKey(string $table, string $name, array|string $columns): static
182
    {
183
        $sql = $this->getQueryBuilder()->addPrimaryKey($table, $name, $columns);
184
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
185
    }
186
187
    public function addUnique(string $table, string $name, array|string $columns): static
188
    {
189
        $sql = $this->getQueryBuilder()->addUnique($table, $name, $columns);
190
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
191
    }
192
193
    public function alterColumn(string $table, string $column, string $type): static
194
    {
195
        $sql = $this->getQueryBuilder()->alterColumn($table, $column, $type);
196
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
197
    }
198
199
    public function batchInsert(string $table, array $columns, iterable $rows): static
200
    {
201
        $table = $this->getQueryBuilder()->quoter()->quoteSql($table);
202
203
        /** @psalm-var string[] $columns */
204
        foreach ($columns as &$column) {
205
            $column = $this->getQueryBuilder()->quoter()->quoteSql($column);
206
        }
207
208
        unset($column);
209
210
        $params = [];
211
        $sql = $this->getQueryBuilder()->batchInsert($table, $columns, $rows, $params);
212
213
        $this->setRawSql($sql);
214
        $this->bindValues($params);
215
216
        return $this;
217
    }
218
219
    abstract public function bindValue(int|string $name, mixed $value, int $dataType = null): static;
220
221
    abstract public function bindValues(array $values): static;
222
223
    public function checkIntegrity(string $schema, string $table, bool $check = true): static
224
    {
225
        $sql = $this->getQueryBuilder()->checkIntegrity($schema, $table, $check);
226
        return $this->setSql($sql);
227
    }
228
229
    public function createIndex(
230
        string $table,
231
        string $name,
232
        array|string $columns,
233
        string $indexType = null,
234
        string $indexMethod = null
235
    ): static {
236
        $sql = $this->getQueryBuilder()->createIndex($table, $name, $columns, $indexType, $indexMethod);
237
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
238
    }
239
240
    public function createTable(string $table, array $columns, string $options = null): static
241
    {
242
        $sql = $this->getQueryBuilder()->createTable($table, $columns, $options);
243
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
244
    }
245
246
    public function createView(string $viewName, QueryInterface|string $subQuery): static
247
    {
248
        $sql = $this->getQueryBuilder()->createView($viewName, $subQuery);
249
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
250
    }
251
252
    public function delete(string $table, array|string $condition = '', array $params = []): static
253
    {
254
        $sql = $this->getQueryBuilder()->delete($table, $condition, $params);
255
        return $this->setSql($sql)->bindValues($params);
256
    }
257
258
    public function dropCheck(string $table, string $name): static
259
    {
260
        $sql = $this->getQueryBuilder()->dropCheck($table, $name);
261
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
262
    }
263
264
    public function dropColumn(string $table, string $column): static
265
    {
266
        $sql = $this->getQueryBuilder()->dropColumn($table, $column);
267
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
268
    }
269
270
    public function dropCommentFromColumn(string $table, string $column): static
271
    {
272
        $sql = $this->getQueryBuilder()->dropCommentFromColumn($table, $column);
273
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
274
    }
275
276
    public function dropCommentFromTable(string $table): static
277
    {
278
        $sql = $this->getQueryBuilder()->dropCommentFromTable($table);
279
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
280
    }
281
282
    public function dropDefaultValue(string $table, string $name): static
283
    {
284
        $sql = $this->getQueryBuilder()->dropDefaultValue($table, $name);
285
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
286
    }
287
288
    public function dropForeignKey(string $table, string $name): static
289
    {
290
        $sql = $this->getQueryBuilder()->dropForeignKey($table, $name);
291
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
292
    }
293
294
    public function dropIndex(string $table, string $name): static
295
    {
296
        $sql = $this->getQueryBuilder()->dropIndex($table, $name);
297
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
298
    }
299
300
    public function dropPrimaryKey(string $table, string $name): static
301
    {
302
        $sql = $this->getQueryBuilder()->dropPrimaryKey($table, $name);
303
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
304
    }
305
306
    public function dropTable(string $table): static
307
    {
308
        $sql = $this->getQueryBuilder()->dropTable($table);
309
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
310
    }
311
312
    public function dropUnique(string $table, string $name): static
313
    {
314
        $sql = $this->getQueryBuilder()->dropUnique($table, $name);
315
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
316
    }
317
318
    public function dropView(string $viewName): static
319
    {
320
        $sql = $this->getQueryBuilder()->dropView($viewName);
321
        return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
322
    }
323
324
    public function getParams(bool $asValues = true): array
325
    {
326
        if (!$asValues) {
327
            return $this->params;
328
        }
329
330
        $buildParams = [];
331
332
        foreach ($this->params as $name => $value) {
333
            /** @psalm-var mixed */
334
            $buildParams[$name] = $value->getValue();
335
        }
336
337
        return $buildParams;
338
    }
339
340
    public function getRawSql(): string
341
    {
342
        if (empty($this->params)) {
343
            return $this->sql;
344
        }
345
346
        $params = [];
347
348
        /** @psalm-var mixed $value */
349
        foreach ($this->params as $name => $value) {
350
            if (is_string($name) && strncmp(':', $name, 1)) {
351
                $name = ':' . $name;
352
            }
353
354
            if ($value instanceof ParamInterface) {
355
                /** @psalm-var mixed $value */
356
                $value = $value->getValue();
357
            }
358
359
            if (is_string($value)) {
360
                /** @psalm-var mixed */
361
                $params[$name] = $this->getQueryBuilder()->quoter()->quoteValue($value);
362
            } elseif (is_bool($value)) {
363
                /** @psalm-var string */
364
                $params[$name] = $value ? 'TRUE' : 'FALSE';
365
            } elseif ($value === null) {
366
                $params[$name] = 'NULL';
367
            } elseif ((!is_object($value) && !is_resource($value)) || $value instanceof Expression) {
368
                /** @psalm-var mixed */
369
                $params[$name] = $value;
370
            }
371
        }
372
373
        if (!isset($params[0])) {
374
            return preg_replace_callback('#(:\w+)#', static function (array $matches) use ($params): string {
375
                $m = $matches[1];
376
                return (string)($params[$m] ?? $m);
377
            }, $this->sql);
378
        }
379
380
        // Support unnamed placeholders should be dropped
381
        $sql = '';
382
383
        foreach (explode('?', $this->sql) as $i => $part) {
384
            $sql .= $part . (string)($params[$i] ?? '');
385
        }
386
387
        return $sql;
388
    }
389
390
    public function getSql(): string
391
    {
392
        return $this->sql;
393
    }
394
395
    public function insert(string $table, QueryInterface|array $columns): static
396
    {
397
        $params = [];
398
        $sql = $this->getQueryBuilder()->insert($table, $columns, $params);
399
        return $this->setSql($sql)->bindValues($params);
400
    }
401
402
    public function insertWithReturningPks(string $table, array $columns): bool|array
403
    {
404
        $params = [];
405
406
        $sql = $this->getQueryBuilder()->insertWithReturningPks($table, $columns, $params);
407
408
        $this->setSql($sql)->bindValues($params);
409
410
        /** @psalm-var array|bool $result */
411
        $result = $this->queryInternal(self::QUERY_MODE_ROW | self::QUERY_MODE_EXECUTE);
412
413
        return is_array($result) ? $result : false;
414
    }
415
416
    public function execute(): int
417
    {
418
        $sql = $this->getSql();
419
420
        if ($sql === '') {
421
            return 0;
422
        }
423
424
        /** @psalm-var int|bool $execute */
425
        $execute = $this->queryInternal(self::QUERY_MODE_EXECUTE);
426
427
        return is_int($execute) ? $execute : 0;
428
    }
429
430
    public function query(): DataReaderInterface
431
    {
432
        /** @psalm-var DataReaderInterface */
433
        return $this->queryInternal(self::QUERY_MODE_CURSOR);
434
    }
435
436
    public function queryAll(): array
437
    {
438
        /** @psalm-var array<array-key, array>|null $results */
439
        $results = $this->queryInternal(self::QUERY_MODE_ALL);
440
        return $results ?? [];
441
    }
442
443
    public function queryColumn(): array
444
    {
445
        /** @psalm-var mixed $results */
446
        $results = $this->queryInternal(self::QUERY_MODE_COLUMN);
447
        return is_array($results) ? $results : [];
448
    }
449
450
    public function queryOne(): array|null
451
    {
452
        /** @psalm-var mixed $results */
453
        $results = $this->queryInternal(self::QUERY_MODE_ROW);
454
        return is_array($results) ? $results : null;
455
    }
456
457
    public function queryScalar(): bool|string|null|int|float
458
    {
459
        /** @psalm-var array|false $firstRow */
460
        $firstRow = $this->queryInternal(self::QUERY_MODE_SCALAR);
461
462
        if (!$firstRow) {
463
            return false;
464
        }
465
466
        /** @psalm-var mixed $result */
467
        $result = current($firstRow);
468
469
        if (is_resource($result) && get_resource_type($result) === 'stream') {
470
            return stream_get_contents($result);
471
        }
472
473
        return is_scalar($result) ? $result : null;
474
    }
475
476
    public function renameColumn(string $table, string $oldName, string $newName): static
477
    {
478
        $sql = $this->getQueryBuilder()->renameColumn($table, $oldName, $newName);
479
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
480
    }
481
482
    public function renameTable(string $table, string $newName): static
483
    {
484
        $sql = $this->getQueryBuilder()->renameTable($table, $newName);
485
        return $this->setSql($sql)->requireTableSchemaRefresh($table);
486
    }
487
488
    public function resetSequence(string $table, int|string $value = null): static
489
    {
490
        $sql = $this->getQueryBuilder()->resetSequence($table, $value);
491
        return $this->setSql($sql);
492
    }
493
494
    public function setRawSql(string $sql): static
495
    {
496
        if ($sql !== $this->sql) {
497
            $this->cancel();
498
            $this->reset();
499
            $this->sql = $sql;
500
        }
501
502
        return $this;
503
    }
504
505
    public function setSql(string $sql): static
506
    {
507
        $this->cancel();
508
        $this->reset();
509
        $this->sql = $this->getQueryBuilder()->quoter()->quoteSql($sql);
510
        return $this;
511
    }
512
513
    public function setRetryHandler(Closure|null $handler): static
514
    {
515
        $this->retryHandler = $handler;
516
        return $this;
517
    }
518
519
    public function truncateTable(string $table): static
520
    {
521
        $sql = $this->getQueryBuilder()->truncateTable($table);
522
        return $this->setSql($sql);
523
    }
524
525
    public function update(string $table, array $columns, array|string $condition = '', array $params = []): static
526
    {
527
        $sql = $this->getQueryBuilder()->update($table, $columns, $condition, $params);
528
        return $this->setSql($sql)->bindValues($params);
529
    }
530
531
    public function upsert(
532
        string $table,
533
        QueryInterface|array $insertColumns,
534
        bool|array $updateColumns = true,
535
        array $params = []
536
    ): static {
537
        $sql = $this->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $params);
538
        return $this->setSql($sql)->bindValues($params);
539
    }
540
541
    /**
542
     * @return QueryBuilderInterface The query builder instance.
543
     */
544
    abstract protected function getQueryBuilder(): QueryBuilderInterface;
545
546
    /**
547
     * Returns the query result.
548
     *
549
     * @param int $queryMode Query mode, `QUERY_MODE_*`.
550
     *
551
     * @throws Exception
552
     * @throws Throwable
553
     */
554
    abstract protected function internalGetQueryResult(int $queryMode): mixed;
555
556
    /**
557
     * Executes a prepared statement.
558
     *
559
     * @param string|null $rawSql The rawSql if it has been created.
560
     *
561
     * @throws Exception
562
     * @throws Throwable
563
     */
564
    abstract protected function internalExecute(string|null $rawSql): void;
565
566
    /**
567
     * Check if the value has a given flag.
568
     *
569
     * @param int $value Flags value to check.
570
     * @param int $flag Flag to look for in the value.
571
     *
572
     * @return bool Whether the value has a given flag.
573
     */
574
    protected function is(int $value, int $flag): bool
575
    {
576
        return ($value & $flag) === $flag;
577
    }
578
579
    /**
580
     * The method is called after the query is executed.
581
     *
582
     * @param int $queryMode Query mode, `QUERY_MODE_*`.
583
     *
584
     * @throws Exception
585
     * @throws Throwable
586
     */
587
    protected function queryInternal(int $queryMode): mixed
588
    {
589
        $isReadMode = $this->isReadMode($queryMode);
590
        $this->prepare($isReadMode);
591
592
        try {
593
            $this->internalExecute($this->getRawSql());
594
595
            /** @psalm-var mixed $result */
596
            $result = $this->internalGetQueryResult($queryMode);
597
598
            if (!$isReadMode) {
599
                $this->refreshTableSchema();
600
            }
601
        } catch (Exception $e) {
602
            throw $e;
603
        }
604
605
        return $result;
606
    }
607
608
    /**
609
     * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}.
610
     */
611
    abstract protected function refreshTableSchema(): void;
612
613
    /**
614
     * Marks a specified table schema to be refreshed after command execution.
615
     *
616
     * @param string $name Name of the table, which schema should be refreshed.
617
     */
618
    protected function requireTableSchemaRefresh(string $name): static
619
    {
620
        $this->refreshTableName = $name;
621
        return $this;
622
    }
623
624
    /**
625
     * Marks the command to execute in transaction.
626
     *
627
     * @param string|null $isolationLevel The isolation level to use for this transaction.
628
     *
629
     * {@see \Yiisoft\Db\Transaction\TransactionInterface::begin()} for details.
630
     */
631
    protected function requireTransaction(string $isolationLevel = null): static
632
    {
633
        $this->isolationLevel = $isolationLevel;
634
        return $this;
635
    }
636
637
    /**
638
     * Resets the command object, so it can be reused to build another SQL statement.
639
     */
640
    protected function reset(): void
641
    {
642
        $this->sql = '';
643
        $this->params = [];
644
        $this->refreshTableName = null;
645
        $this->isolationLevel = null;
646
        $this->retryHandler = null;
647
    }
648
649
    /**
650
     * Checks if the query mode is a read mode.
651
     */
652
    private function isReadMode(int $queryMode): bool
653
    {
654
        return !$this->is($queryMode, self::QUERY_MODE_EXECUTE);
655
    }
656
}
657