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