Passed
Pull Request — master (#785)
by Sergei
02:19
created

AbstractCommand::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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