AbstractCommand::getRawSql()   B
last analyzed

Complexity

Conditions 8
Paths 10

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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