Passed
Pull Request — master (#455)
by Wilmer
27:34 queued 24:54
created

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