Passed
Pull Request — master (#801)
by Sergei
02:27
created

Quoter::quoteTableName()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 21
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use Yiisoft\Db\Exception\InvalidArgumentException;
8
use Yiisoft\Db\Expression\ExpressionInterface;
9
10
use function addcslashes;
11
use function array_slice;
12
use function count;
13
use function explode;
14
use function implode;
15
use function is_string;
16
use function preg_match;
17
use function preg_replace;
18
use function preg_replace_callback;
19
use function str_contains;
20
use function str_replace;
21
use function str_starts_with;
22
use function strrpos;
23
use function substr;
24
25
/**
26
 * The Quoter is a class that's used to quote table and column names for use in SQL statements.
27
 *
28
 * It provides a set of methods for quoting different types of names, such as table names, column names, and schema
29
 * names.
30
 *
31
 * The Quoter class is used by {@see \Yiisoft\Db\QueryBuilder\AbstractQueryBuilder} to quote names.
32
 *
33
 * It's also used by {@see \Yiisoft\Db\Command\AbstractCommand} to quote names in SQL statements before passing them to
34
 * database servers.
35
 */
36
class Quoter implements QuoterInterface
37
{
38
    public function __construct(
39
        /** @psalm-var string[]|string */
40
        private array|string $columnQuoteCharacter,
41
        /** @psalm-var string[]|string */
42
        private array|string $tableQuoteCharacter,
43
        private string $tablePrefix = ''
44
    ) {
45
    }
46
47
    public function cleanUpTableNames(array $tableNames): array
48
    {
49
        $cleanedUpTableNames = [];
50
        $pattern = <<<PATTERN
51
        ~^\s*((?:['"`\[]|{{).*?(?:['"`\]]|}})|\(.*?\)|.*?)(?:\s+(?:as\s+)?((?:['"`\[]|{{).*?(?:['"`\]]|}})|.*?))?\s*$~iux
52
        PATTERN;
53
54
        /** @psalm-var array<array-key, ExpressionInterface|string> $tableNames */
55
        foreach ($tableNames as $alias => $tableName) {
56
            if (is_string($tableName) && !is_string($alias)) {
57
                if (preg_match($pattern, $tableName, $matches)) {
58
                    if (isset($matches[2])) {
59
                        [, $tableName, $alias] = $matches;
60
                    } else {
61
                        $tableName = $alias = $matches[1];
62
                    }
63
                }
64
            }
65
66
            if (!is_string($alias)) {
67
                throw new InvalidArgumentException(
68
                    'To use Expression in from() method, pass it in array format with alias.'
69
                );
70
            }
71
72
            if (is_string($tableName)) {
73
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $this->ensureNameQuoted($tableName);
74
            } elseif ($tableName instanceof ExpressionInterface) {
75
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
76
            } else {
77
                throw new InvalidArgumentException(
78
                    'Use ExpressionInterface without cast to string as object of tableName'
79
                );
80
            }
81
        }
82
83
        return $cleanedUpTableNames;
84
    }
85
86
    public function getRawTableName(string $name): string
87
    {
88
        if (str_contains($name, '{{')) {
89
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
90
91
            return str_replace('%', $this->tablePrefix, $name);
92
        }
93
94
        return $name;
95
    }
96
97
    public function getTableNameParts(string $name, bool $withColumn = false): array
98
    {
99
        $parts = array_slice(explode('.', $name), -2, 2);
100
101
        return $this->unquoteParts($parts, $withColumn);
102
    }
103
104
    public function ensureNameQuoted(string $name): string
105
    {
106
        $name = str_replace(["'", '"', '`', '[', ']'], '', $name);
107
108
        if ($name && !preg_match('/^{{.*}}$/', $name)) {
109
            return '{{' . $name . '}}';
110
        }
111
112
        return $name;
113
    }
114
115
    public function ensureColumnName(string $name): string
116
    {
117
        if (strrpos($name, '.') !== false) {
118
            $parts = explode('.', $name);
119
            $name = $parts[count($parts) - 1];
120
        }
121
122
        return preg_replace('|^\[\[([\w\-. ]+)]]$|', '\1', $name);
123
    }
124
125
    public function quoteColumnName(string $name): string
126
    {
127
        if (str_contains($name, '(') || str_contains($name, '[[')) {
128
            return $name;
129
        }
130
131
        if (($pos = strrpos($name, '.')) !== false) {
132
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
133
            $name = substr($name, $pos + 1);
134
        } else {
135
            $prefix = '';
136
        }
137
138
        if (str_contains($name, '{{')) {
139
            return $name;
140
        }
141
142
        return $prefix . $this->quoteSimpleColumnName($name);
143
    }
144
145
    public function quoteSimpleColumnName(string $name): string
146
    {
147
        if (is_string($this->columnQuoteCharacter)) {
148
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
149
        } else {
150
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
151
        }
152
153
        return $name === '*' || str_starts_with($name, $startingCharacter)
154
            ? $name
155
            : $startingCharacter . $name . $endingCharacter;
156
    }
157
158
    public function quoteSimpleTableName(string $name): string
159
    {
160
        if (is_string($this->tableQuoteCharacter)) {
161
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
162
        } else {
163
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
164
        }
165
166
        return str_starts_with($name, $startingCharacter)
167
            ? $name
168
            : $startingCharacter . $name . $endingCharacter;
169
    }
170
171
    public function quoteSql(string $sql): string
172
    {
173
        return preg_replace_callback(
174
            '/({{(%?[\w\-. ]+)%?}}|\\[\\[([\w\-. ]+)]])/',
175
            function ($matches) {
176
                if (isset($matches[3])) {
177
                    return $this->quoteColumnName($matches[3]);
178
                }
179
180
                return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
181
            },
182
            $sql
183
        );
184
    }
185
186
    public function quoteTableName(string $name): string
187
    {
188
        if (str_starts_with($name, '(')) {
189
            return $name;
190
        }
191
192
        if (str_contains($name, '{{')) {
193
            return $name;
194
        }
195
196
        if (!str_contains($name, '.')) {
197
            return $this->quoteSimpleTableName($name);
198
        }
199
200
        $parts = $this->getTableNameParts($name);
201
202
        foreach ($parts as $i => $part) {
203
            $parts[$i] = $this->quoteSimpleTableName($part);
204
        }
205
206
        return implode('.', $parts);
207
    }
208
209
    public function quoteValue(mixed $value): mixed
210
    {
211
        if (!is_string($value)) {
212
            return $value;
213
        }
214
215
        return "'" . str_replace("'", "''", addcslashes($value, "\000\032")) . "'";
216
    }
217
218
    public function setTablePrefix(string $value): void
219
    {
220
        $this->tablePrefix = $value;
221
    }
222
223
    public function unquoteSimpleColumnName(string $name): string
224
    {
225
        if (is_string($this->columnQuoteCharacter)) {
226
            $startingCharacter = $this->columnQuoteCharacter;
227
        } else {
228
            $startingCharacter = $this->columnQuoteCharacter[0];
229
        }
230
231
        return !str_starts_with($name, $startingCharacter)
232
            ? $name
233
            : substr($name, 1, -1);
234
    }
235
236
    public function unquoteSimpleTableName(string $name): string
237
    {
238
        if (is_string($this->tableQuoteCharacter)) {
239
            $startingCharacter = $this->tableQuoteCharacter;
240
        } else {
241
            $startingCharacter = $this->tableQuoteCharacter[0];
242
        }
243
244
        return !str_starts_with($name, $startingCharacter)
245
            ? $name
246
            : substr($name, 1, -1);
247
    }
248
249
    /**
250
     * @psalm-param string[] $parts Parts of table name
251
     *
252
     * @psalm-return string[]
253
     */
254
    protected function unquoteParts(array $parts, bool $withColumn): array
255
    {
256
        $lastKey = count($parts) - 1;
257
258
        foreach ($parts as $k => &$part) {
259
            $part = ($withColumn && $lastKey === $k) ?
260
                $this->unquoteSimpleColumnName($part) :
261
                $this->unquoteSimpleTableName($part);
262
        }
263
264
        return $parts;
265
    }
266
}
267