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

ColumnSchema::phpTypecastComposite()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 31
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

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