Completed
Push — develop ( 705c20...1dfcbc )
by Freddie
03:12
created

SchemaAttribute::name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
b 0
f 0
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of FlexPHP.
4
 *
5
 * (c) Freddie Gar <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace FlexPHP\Schema;
11
12
use FlexPHP\Schema\Constants\Keyword;
13
use FlexPHP\Schema\Constants\Rule;
14
use FlexPHP\Schema\Exception\InvalidSchemaAttributeException;
15
use FlexPHP\Schema\Validations\SchemaAttributeValidation;
16
17
final class SchemaAttribute implements SchemaAttributeInterface
18
{
19
    /**
20
     * @var string
21
     */
22
    private $name;
23
24
    /**
25
     * @var string
26
     */
27
    private $dataType;
28
29
    /**
30
     * @var array<string, mixed>
31
     */
32
    private $constraints = [];
33
34
    /**
35
     * @param mixed $constraints
36
     */
37 116
    public function __construct(string $name, string $dataType, $constraints = null)
38
    {
39 116
        $this->setName($name);
40 116
        $this->setDataType($dataType);
41 116
        $this->setConstraints($constraints);
42
43 116
        $this->validate();
44 113
        $this->validateLogic();
45 86
    }
46
47 116
    public function name(): string
48
    {
49 116
        return $this->name;
50
    }
51
52 116
    public function dataType(): string
53
    {
54 116
        return $this->dataType;
55
    }
56
57 116
    public function constraints(): array
58
    {
59 116
        return $this->constraints;
60
    }
61
62 6
    public function type(): ?string
63
    {
64 6
        return $this->constraints[Rule::TYPE] ?? null;
65
    }
66
67 29
    public function isRequired(): bool
68
    {
69 29
        return (bool)($this->constraints[Rule::REQUIRED] ?? false);
70
    }
71
72 66
    public function minLength(): ?int
73
    {
74 66
        return $this->constraints[Rule::MINLENGTH] ?? null;
75
    }
76
77 61
    public function maxLength(): ?int
78
    {
79 61
        return $this->constraints[Rule::MAXLENGTH] ?? null;
80
    }
81
82 24
    public function minCheck(): ?int
83
    {
84 24
        return $this->constraints[Rule::MINCHECK] ?? null;
85
    }
86
87 23
    public function maxCheck(): ?int
88
    {
89 23
        return $this->constraints[Rule::MAXCHECK] ?? null;
90
    }
91
92 55
    public function min(): ?int
93
    {
94 55
        return $this->constraints[Rule::MIN] ?? null;
95
    }
96
97 53
    public function max(): ?int
98
    {
99 53
        return $this->constraints[Rule::MAX] ?? null;
100
    }
101
102 6
    public function equalTo(): ?string
103
    {
104 6
        return $this->constraints[Rule::EQUALTO] ?? null;
105
    }
106
107 97
    public function isPk(): bool
108
    {
109 97
        return (bool)($this->constraints[Rule::PRIMARYKEY] ?? false);
110
    }
111
112 94
    public function isAi(): bool
113
    {
114 94
        return (bool)($this->constraints[Rule::AUTOINCREMENT] ?? false);
115
    }
116
117 15
    public function isFk(): bool
118
    {
119 15
        return (bool)($this->constraints[Rule::FOREIGNKEY] ?? false);
120
    }
121
122 9
    public function fkTable(): ?string
123
    {
124 9
        return $this->constraints[Rule::FOREIGNKEY]['table'] ?? null;
125
    }
126
127 9
    public function fkId(): ?string
128
    {
129 9
        return $this->constraints[Rule::FOREIGNKEY]['id'] ?? null;
130
    }
131
132 9
    public function fkName(): ?string
133
    {
134 9
        return $this->constraints[Rule::FOREIGNKEY]['name'] ?? null;
135
    }
136
137 92
    public function isCa(): bool
138
    {
139 92
        return (bool)($this->constraints[Rule::CREATEDAT] ?? false);
140
    }
141
142 91
    public function isUa(): bool
143
    {
144 91
        return (bool)($this->constraints[Rule::UPDATEDAT] ?? false);
145
    }
146
147 92
    public function isBlame(): bool
148
    {
149 92
        return $this->isCa() || $this->isUa();
150
    }
151
152 116
    public function properties(): array
153
    {
154
        return [
155 116
            Keyword::NAME => $this->name(),
156 116
            Keyword::DATATYPE => $this->dataType(),
157 116
            Keyword::CONSTRAINTS => $this->constraints(),
158
        ];
159
    }
160
161 80
    public function typeHint(): string
162
    {
163
        $typeHintByDataType = [
164 80
            'smallint' => 'int',
165
            'integer' => 'int',
166
            'float' => 'float',
167
            'double' => 'float',
168
            'bool' => 'bool',
169
            'boolean' => 'bool',
170
            'date' => '\DateTime',
171
            'date_immutable' => '\DateTimeImmutable',
172
            'datetime' => '\DateTime',
173
            'datetime_immutable' => '\DateTimeImmutable',
174
            'datetimetz' => '\DateTime',
175
            'datetimetz_immutable' => '\DateTimeImmutable',
176
            'time' => '\DateTime',
177
            'time_immutable' => '\DateTimeImmutable',
178
            'array' => 'array',
179
            'simple_array' => 'array',
180
            'json_array' => 'array',
181
        ];
182
183 80
        if (isset($typeHintByDataType[$this->dataType()])) {
184 49
            return $typeHintByDataType[$this->dataType()];
185
        }
186
187 35
        return 'string';
188
    }
189
190 116
    private function validate(): void
191
    {
192 116
        (new SchemaAttributeValidation($this->properties()))->validate();
193 113
    }
194
195 113
    private function validateLogic(): void
196
    {
197 113
        if (empty($this->constraints())) {
198 19
            return;
199
        }
200
201 96
        $name = 'Logic: [' . $this->name() . '] ';
202
203 96
        if ($this->isPk() && !$this->isRequired()) {
204 3
            throw new InvalidSchemaAttributeException($name . 'Primary Key must be required.');
205
        }
206
207 93
        if ($this->isAi() && !$this->isPk()) {
208 1
            throw new InvalidSchemaAttributeException($name . 'Autoincrement must be Primary Key too.');
209
        }
210
211 92
        if ($this->isAi() && !\in_array($this->dataType(), ['smallint', 'integer', 'bigint'])) {
212 1
            throw new InvalidSchemaAttributeException($name . 'Autoincrement must be a numeric value.');
213
        }
214
215 91
        if ($this->isAi() && $this->isFk()) {
216 1
            throw new InvalidSchemaAttributeException($name . 'Foreign Key cannot be autoincrement.');
217
        }
218
219 90
        if ($this->isBlame() && \strpos($this->typeHint(), '\Date') === false) {
220 2
            throw new InvalidSchemaAttributeException($name . 'Blame property must be date datetype valid.');
221
        }
222
223 88
        if ($this->isCa() && $this->isUa()) {
224 1
            throw new InvalidSchemaAttributeException($name . 'Created and Updated At in same property is not valid.');
225
        }
226
227 87
        if (\in_array($this->dataType(), ['smallint', 'integer', 'bigint', 'double', 'float'])
228 87
            && ($this->minLength() || $this->maxLength())
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->maxLength() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->minLength() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
229
        ) {
230 10
            throw new InvalidSchemaAttributeException($name . 'Numeric property use: min, max.');
231
        }
232
233 77
        if ($this->dataType() !== 'bigint' && $this->typeHint() === 'string' && ($this->min() || $this->max())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->min() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->max() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
234 4
            throw new InvalidSchemaAttributeException($name . 'String properties use minlength, maxlength.');
235
        }
236
237 73
        if ((\strpos($this->typeHint(), '\Date') !== false || \in_array($this->dataType(), ['bool', 'blob']))
238 17
            && ($this->min() || $this->max() || $this->minLength() || $this->maxLength() || $this->minCheck()
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->max() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->min() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->maxLength() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->minCheck() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->minLength() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
239 73
                || $this->maxCheck()
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->maxCheck() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
240
        )) {
241 4
            throw new InvalidSchemaAttributeException($name . 'Date, bool, blob properties not use min, max, etc');
242
        }
243 69
    }
244
245 116
    private function setName(string $name): void
246
    {
247 116
        $this->name = $name;
248 116
    }
249
250 116
    private function setDataType(string $dataType): void
251
    {
252 116
        $this->dataType = $dataType;
253 116
    }
254
255
    /**
256
     * @param mixed $constraints
257
     */
258 116
    private function setConstraints($constraints): void
259
    {
260 116
        if (!empty($constraints)) {
261 96
            if (\is_string($constraints)) {
262 60
                $this->setConstraintsFromString($constraints);
263
            } else {
264 36
                $this->setConstraintsFromArray($constraints);
265
            }
266
        }
267 116
    }
268
269 60
    private function setConstraintsFromString(string $constraints): void
270
    {
271 60
        $this->setConstraintsFromArray($this->getConstraintsFromString($constraints));
272 60
    }
273
274 96
    private function setConstraintsFromArray(array $constraints): void
275
    {
276 96
        $this->constraints = $this->getConstraintsCast($constraints);
277 96
    }
278
279 60
    private function getConstraintsFromString(string $constraints): array
280
    {
281 60
        $_constraints = \explode('|', $constraints);
282
283
        /** @var mixed $_constraint */
284 60
        foreach ($_constraints as $index => $_constraint) {
285 60
            $_rule = \explode(':', $_constraint);
286
287 60
            if (\count($_rule) === 2) {
288 49
                [$_name, $_options] = $_rule;
289
290 49
                if (Rule::FOREIGNKEY !== $_name && \strpos($_options, ',') !== false) { // Range
291 2
                    [$min, $max] = \explode(',', $_options);
292 2
                    $_options = \compact('min', 'max');
293 47
                } elseif (\preg_match('/^false$/i', $_options)) { // False as string
294 10
                    $_options = false;
295 40
                } elseif (\preg_match('/^true$/i', $_options)) { // True as string
296 9
                    $_options = true;
297
                }
298
299 49
                $_constraints[$_name] = $_options;
300
            } else {
301 22
                $_constraints[$_rule[0]] = true;
302
            }
303
304 60
            unset($_constraints[$index]);
305
        }
306
307 60
        return $_constraints;
308
    }
309
310 96
    private function getConstraintsCast(array $constraints): array
311
    {
312 96
        foreach ($constraints as $name => $value) {
313 96
            if (\is_int($name)) {
314 11
                $constraints[$value] = true;
315 11
                unset($constraints[$name]);
316 91
            } elseif ($name === Rule::CHECK || $name === Rule::LENGTH) {
317 4
                $constraints['min' . $name] = (int)$value['min'];
318 4
                $constraints['max' . $name] = (int)$value['max'];
319 4
                unset($constraints[$name]);
320 87
            } elseif ($name === Rule::FOREIGNKEY && \is_string($value)) {
321 9
                $constraints[$name] = $this->getFkOptions($value);
322
            } else {
323 96
                $constraints[$name] = \is_numeric($value) ? (int)$value : $value;
324
            }
325
        }
326
327 96
        return $constraints;
328
    }
329
330 9
    private function getFkOptions(string $constraint): array
331
    {
332 9
        $_vars = \explode(',', $constraint);
333 9
        $fkName = 'name';
334 9
        $fkId = 'id';
335
336 9
        switch (\count($_vars)) {
337 9
            case 3:
338 2
                [$fkTable, $fkName, $fkId] = $_vars;
339
340 2
                break;
341 7
            case 2:
342 4
                [$fkTable, $fkName] = $_vars;
343
344 4
                break;
345
            default:
346 3
                [$fkTable] = $_vars;
347
348 3
                break;
349
        }
350
351
        return [
352 9
            'table' => $fkTable,
353 9
            'name' => $fkName,
354 9
            'id' => $fkId,
355
        ];
356
    }
357
}
358