Passed
Push — master ( 22c6d1...ab49a2 )
by Def
02:32
created

Quoter::unquoteSimpleColumnName()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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