Passed
Pull Request — master (#303)
by
unknown
03:47
created

ColumnSchema::dbTypecast()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 7
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 5
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql;
6
7
use JsonException;
8
use PDO;
9
use Yiisoft\Db\Command\Param;
10
use Yiisoft\Db\Expression\ArrayExpression;
11
use Yiisoft\Db\Expression\ExpressionInterface;
12
use Yiisoft\Db\Expression\JsonExpression;
13
use Yiisoft\Db\Schema\AbstractColumnSchema;
14
use Yiisoft\Db\Schema\ColumnSchemaInterface;
15
use Yiisoft\Db\Schema\SchemaInterface;
16
17
use function array_walk_recursive;
18
use function bindec;
19
use function decbin;
20
use function is_array;
21
use function is_int;
22
use function is_string;
23
use function json_decode;
24
use function str_pad;
25
26
/**
27
 * Represents the metadata of a column in a database table for PostgreSQL Server.
28
 *
29
 * It provides information about the column's name, type, size, precision, and other details.
30
 *
31
 * It's used to store and retrieve metadata about a column in a database table and is typically used in conjunction with
32
 * the {@see TableSchema}, which represents the metadata of a database table as a whole.
33
 *
34
 * The following code shows how to use:
35
 *
36
 * ```php
37
 * use Yiisoft\Db\Pgsql\ColumnSchema;
38
 *
39
 * $column = new ColumnSchema();
40
 * $column->name('id');
41
 * $column->allowNull(false);
42
 * $column->dbType('integer');
43
 * $column->phpType('integer');
44
 * $column->type('integer');
45
 * $column->defaultValue(0);
46
 * $column->autoIncrement(true);
47
 * $column->primaryKey(true);
48
 * ```
49
 */
50
final class ColumnSchema extends AbstractColumnSchema
51
{
52
    /**
53
     * @var int The dimension of array. Defaults to 0, means this column isn't an array.
54
     */
55
    private int $dimension = 0;
56
57
    /**
58
     * @var string|null Name of an associated sequence if column is auto incremental.
59
     */
60
    private string|null $sequenceName = null;
61
62
    /**
63
     * @var ColumnSchemaInterface[]|null Columns metadata of the composite type.
64
     * @psalm-var array<string, ColumnSchemaInterface>|null
65
     */
66
    private array|null $columns = null;
67
68
    /**
69
     * Converts the input value according to {@see type} and {@see dbType} for use in a db query.
70
     *
71
     * If the value is null or an {@see Expression}, it won't be converted.
72
     *
73
     * @param mixed $value input value
74
     *
75
     * @return mixed Converted value. This may also be an array containing the value as the first element and the PDO
76
     * type as the second element.
77
     */
78 89
    public function dbTypecast(mixed $value): mixed
79
    {
80 89
        if ($this->dimension > 0) {
81 4
            if ($value === null || $value instanceof ExpressionInterface) {
82 2
                return $value;
83
            }
84
85 3
            if ($this->getType() === Schema::TYPE_COMPOSITE) {
86 1
                $value = $this->dbTypecastArray($value, $this->dimension);
87
            }
88
89 3
            return new ArrayExpression($value, $this->getDbType(), $this->dimension);
90
        }
91
92 88
        return $this->dbTypecastValue($value);
93
    }
94
95
    /**
96
     * @param int $dimension Should be more than 0
97
     */
98 1
    private function dbTypecastArray(mixed $value, int $dimension): array
99
    {
100 1
        $items = [];
101
102 1
        if (!is_iterable($value)) {
103
            return $items;
104
        }
105
106
        /** @psalm-var mixed $val */
107 1
        foreach ($value as $val) {
108 1
            if ($dimension > 1) {
109
                $items[] = $this->dbTypecastArray($val, $dimension - 1);
110
            } else {
111
                /** @psalm-suppress MixedAssignment */
112 1
                $items[] = $this->dbTypecastValue($val);
113
            }
114
        }
115
116 1
        return $items;
117
    }
118
119 88
    private function dbTypecastValue(mixed $value): mixed
120
    {
121 88
        if ($value === null || $value instanceof ExpressionInterface) {
122 30
            return $value;
123
        }
124
125 83
        return match ($this->getType()) {
126 83
            SchemaInterface::TYPE_JSON => new JsonExpression($value, $this->getDbType()),
127
128 83
            SchemaInterface::TYPE_BINARY => is_string($value)
129 3
                ? new Param($value, PDO::PARAM_LOB) // explicitly setup PDO param type for binary column
130 3
                : $this->typecast($value),
131
132 83
            Schema::TYPE_BIT => is_int($value)
133 1
                ? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT)
134 1
                : $this->typecast($value),
135
136 83
            Schema::TYPE_COMPOSITE => new CompositeExpression($value, $this->getDbType(), $this->columns),
137
138 83
            default => $this->typecast($value),
139 83
        };
140
    }
141
142
    /**
143
     * Converts the input value according to {@see phpType} after retrieval from the database.
144
     *
145
     * If the value is null or an {@see Expression}, it won't be converted.
146
     *
147
     * @param mixed $value The input value
148
     *
149
     * @throws JsonException
150
     *
151
     * @return mixed The converted value
152
     */
153 97
    public function phpTypecast(mixed $value): mixed
154
    {
155 97
        if ($this->dimension > 0) {
156 12
            if (is_string($value)) {
157 12
                $value = $this->getArrayParser()->parse($value);
158
            }
159
160 12
            if (is_array($value)) {
161 12
                array_walk_recursive($value, function (string|null &$val) {
162
                    /** @psalm-var mixed $val */
163 12
                    $val = $this->phpTypecastValue($val);
164 12
                });
165
            } else {
166 1
                return null;
167
            }
168
169 12
            return $value;
170
        }
171
172 91
        return $this->phpTypecastValue($value);
173
    }
174
175
    /**
176
     * Casts $value after retrieving from the DBMS to PHP representation.
177
     *
178
     * @throws JsonException
179
     */
180 97
    protected function phpTypecastValue(mixed $value): mixed
181
    {
182 97
        if ($value === null) {
183 11
            return null;
184
        }
185
186 97
        return match ($this->getType()) {
187 97
            Schema::TYPE_BIT => is_string($value) ? bindec($value) : $value,
188
189 97
            SchemaInterface::TYPE_BOOLEAN => $value && $value !== 'f',
190
191 97
            SchemaInterface::TYPE_JSON
192 97
                => json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR),
193
194 97
            Schema::TYPE_COMPOSITE => $this->phpTypecastComposite($value),
195
196 97
            default => parent::phpTypecast($value),
197 97
        };
198
    }
199
200 5
    private function phpTypecastComposite(mixed $value): array|null
201
    {
202 5
        if ($this->columns === null) {
203
            return null;
204
        }
205
206 5
        if (is_string($value)) {
207 5
            $value = $this->getCompositeParser()->parse($value);
208
        }
209
210 5
        if (!is_iterable($value)) {
211
            return null;
212
        }
213
214 5
        $fields = [];
215 5
        $columnNames = array_keys($this->columns);
216
217
        /**
218
         * @psalm-var int|string $key
219
         * @psalm-var mixed $val
220
         */
221 5
        foreach ($value as $key => $val) {
222 5
            $columnName = $columnNames[$key] ?? $key;
223
224 5
            if (isset($this->columns[$columnName])) {
225
                /** @psalm-suppress MixedAssignment */
226 5
                $fields[$columnName] = $this->columns[$columnName]->phpTypecast($val);
227
            }
228
        }
229
230 5
        return $fields;
231
    }
232
233
    /**
234
     * Creates instance of ArrayParser.
235
     */
236 12
    protected function getArrayParser(): ArrayParser
237
    {
238 12
        return new ArrayParser();
239
    }
240
241
    /**
242
     * @return int Get the dimension of the array.
243
     *
244
     * Defaults to 0, means this column isn't an array.
245
     */
246 6
    public function getDimension(): int
247
    {
248 6
        return $this->dimension;
249
    }
250
251
    /**
252
     * @return string|null name of an associated sequence if column is auto incremental.
253
     */
254 99
    public function getSequenceName(): string|null
255
    {
256 99
        return $this->sequenceName;
257
    }
258
259
    /**
260
     * Set dimension of an array.
261
     *
262
     * Defaults to 0, means this column isn't an array.
263
     */
264 160
    public function dimension(int $dimension): void
265
    {
266 160
        $this->dimension = $dimension;
267
    }
268
269
    /**
270
     * Set the name of an associated sequence if a column is auto incremental.
271
     */
272 84
    public function sequenceName(string|null $sequenceName): void
273
    {
274 84
        $this->sequenceName = $sequenceName;
275
    }
276
277
    /**
278
     * @param ColumnSchemaInterface[]|null $columns The columns metadata of the composite type.
279
     * @psalm-param array<string, ColumnSchemaInterface>|null $columns
280
     */
281 5
    public function columns(array|null $columns): void
282
    {
283 5
        $this->columns = $columns;
284
    }
285
286
    /**
287
     * @return ColumnSchemaInterface[]|null Columns metadata of the composite type.
288
     */
289 5
    public function getColumns(): array|null
290
    {
291 5
        return $this->columns;
292
    }
293
294
    /**
295
     * Creates instance of CompositeParser.
296
     */
297 5
    private function getCompositeParser(): CompositeParser
298
    {
299 5
        return new CompositeParser();
300
    }
301
}
302