Passed
Push — master ( 4a78e7...e96213 )
by Def
24:10 queued 21:57
created

AbstractCommand::getFromCacheInfo()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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