Passed
Pull Request — 2.x (#65)
by Maxim
12:09
created

Compiler::renderTypecastHandler()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 7
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 16
ccs 0
cts 0
cp 0
crap 30
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Schema;
6
7
use Cycle\ORM\Mapper\Mapper;
8
use Cycle\ORM\Parser\Typecast;
9
use Cycle\ORM\SchemaInterface as Schema;
10
use Cycle\ORM\Select\Repository;
11
use Cycle\ORM\Select\Source;
12
use Cycle\Schema\Definition\Comparator\FieldComparator;
13
use Cycle\Schema\Definition\Entity;
14
use Cycle\Schema\Definition\Field;
15
use Cycle\Schema\Definition\Inheritance\JoinedTable;
16
use Cycle\Schema\Definition\Inheritance\SingleTable;
17
use Cycle\Schema\Exception\CompilerException;
18
use Cycle\Schema\Exception\SchemaModifierException;
19
use Cycle\Schema\Exception\TableInheritance\DiscriminatorColumnNotPresentException;
20
use Cycle\Schema\Exception\TableInheritance\WrongDiscriminatorColumnException;
21
use Cycle\Schema\Exception\TableInheritance\WrongParentKeyColumnException;
22
use Throwable;
23
24
final class Compiler
25
{
26
    /** @var array<non-empty-string, array<int, mixed>> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string, array<int, mixed>> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string, array<int, mixed>>.
Loading history...
27
    private array $result = [];
28
29
    /** @var array<int, mixed> */
30
    private array $defaults = [
31
        Schema::MAPPER => Mapper::class,
32
        Schema::REPOSITORY => Repository::class,
33
        Schema::SOURCE => Source::class,
34
        Schema::SCOPE => null,
35
        Schema::TYPECAST_HANDLER => null,
36
    ];
37
38
    /**
39
     * Compile the registry schema.
40
     *
41
     * @param GeneratorInterface[] $generators
42 918
     */
43
    public function compile(Registry $registry, array $generators = [], array $defaults = []): array
44 918
    {
45
        $this->defaults = $defaults + $this->defaults;
46 918
47 654
        foreach ($generators as $generator) {
48 2
            if (!$generator instanceof GeneratorInterface) {
49 2
                throw new CompilerException(
50 2
                    sprintf(
51 2
                        'Invalid generator `%s`. It should implement `%s` interface.',
52 2
                        \is_object($generator) ? $generator::class : \var_export($generator, true),
53
                        GeneratorInterface::class
54
                    )
55
                );
56
            }
57 652
58
            $registry = $generator->run($registry);
59
        }
60 788
61 788
        foreach ($registry->getIterator() as $entity) {
62 788
            if ($entity->hasPrimaryKey() || $entity->isChildOfSingleTableInheritance()) {
63
                $this->compute($registry, $entity);
64
            }
65
        }
66 780
67
        return $this->result;
68
    }
69
70
    /**
71
     * Get compiled schema result.
72 16
     */
73
    public function getSchema(): array
74 16
    {
75
        return $this->result;
76
    }
77
78
    /**
79
     * Compile entity and relation definitions into packed ORM schema.
80 788
     */
81
    private function compute(Registry $registry, Entity $entity): void
82 788
    {
83 788
        $schema = [
84 788
            Schema::ENTITY => $entity->getClass(),
85 788
            Schema::SOURCE => $entity->getSource() ?? $this->defaults[Schema::SOURCE],
86 788
            Schema::MAPPER => $entity->getMapper() ?? $this->defaults[Schema::MAPPER],
87 788
            Schema::REPOSITORY => $entity->getRepository() ?? $this->defaults[Schema::REPOSITORY],
88 788
            Schema::SCOPE => $entity->getScope() ?? $this->defaults[Schema::SCOPE],
89 788
            Schema::SCHEMA => $entity->getSchema(),
90 788
            Schema::TYPECAST_HANDLER => $this->renderTypecastHandler($entity),
91 788
            Schema::PRIMARY_KEY => $entity->getPrimaryFields()->getNames(),
92 788
            Schema::COLUMNS => $this->renderColumns($entity),
93 788
            Schema::FIND_BY_KEYS => $this->renderReferences($entity),
94 788
            Schema::TYPECAST => $this->renderTypecast($entity),
95
            Schema::RELATIONS => [],
96
        ];
97
98 788
        // For table inheritance we need to fill specific schema segments
99 788
        $inheritance = $entity->getInheritance();
100
        if ($inheritance instanceof SingleTable) {
101 8
            // Check if discriminator column defined and is not null or empty
102 8
            $discriminator = $inheritance->getDiscriminator();
103 2
            if ($discriminator === null || $discriminator === '') {
104
                throw new DiscriminatorColumnNotPresentException($entity);
105 6
            }
106 2
            if (!$entity->getFields()->has($discriminator)) {
107
                throw new WrongDiscriminatorColumnException($entity, $discriminator);
108
            }
109 4
110 4
            $schema[Schema::CHILDREN] = $inheritance->getChildren();
111 784
            $schema[Schema::DISCRIMINATOR] = $discriminator;
112 6
        } elseif ($inheritance instanceof JoinedTable) {
113
            $schema[Schema::PARENT] = $inheritance->getParent()->getRole();
114
            assert($schema[Schema::PARENT] !== null);
115 6
116 6
            $parent = $registry->getEntity($schema[Schema::PARENT]);
0 ignored issues
show
Bug introduced by
It seems like $schema[Cycle\ORM\SchemaInterface::PARENT] can also be of type array and array and array and null; however, parameter $role of Cycle\Schema\Registry::getEntity() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

116
            $parent = $registry->getEntity(/** @scrutinizer ignore-type */ $schema[Schema::PARENT]);
Loading history...
117 4
            if ($inheritance->getOuterKey()) {
118 2
                if (!$parent->getFields()->has($inheritance->getOuterKey())) {
119
                    throw new WrongParentKeyColumnException($parent, $inheritance->getOuterKey());
120 2
                }
121
                $schema[Schema::PARENT_KEY] = $inheritance->getOuterKey();
122
            }
123
        }
124 784
125
        $this->renderRelations($registry, $entity, $schema);
126 784
127 768
        if ($registry->hasTable($entity)) {
128 768
            $schema[Schema::DATABASE] = $registry->getDatabase($entity);
129
            $schema[Schema::TABLE] = $registry->getTable($entity);
130
        }
131
132 784
        // Apply modifiers
133
        foreach ($entity->getSchemaModifiers() as $modifier) {
134 6
            try {
135 2
                $modifier->modifySchema($schema);
136 2
            } catch (Throwable $e) {
137 2
                throw new SchemaModifierException(
138 2
                    sprintf(
139 2
                        'Unable to apply schema modifier `%s` for the `%s` role. %s',
140 2
                        $modifier::class,
141 2
                        $entity->getRole(),
142
                        $e->getMessage()
143 2
                    ),
144
                    (int)$e->getCode(),
145
                    $e
146
                );
147
            }
148
        }
149
150 782
        // For STI child we need only schema role as a key and entity segment
151 4
        if ($entity->isChildOfSingleTableInheritance()) {
152
            $schema = \array_intersect_key($schema, [Schema::ENTITY, Schema::ROLE]);
153
        }
154
155 782
        /** @var array<int, mixed> $schema */
156 782
        ksort($schema);
157
        $role = $entity->getRole();
158 782
        assert(!empty($role));
159 782
        $this->result[$role] = $schema;
160
    }
161 788
162
    private function renderColumns(Entity $entity): array
163
    {
164
        // Check field duplicates
165 788
        /** @var Field[][] $fieldGroups */
166
        $fieldGroups = [];
167 788
        // Collect and group fields by column name
168 788
        foreach ($entity->getFields() as $name => $field) {
169
            $fieldGroups[$field->getColumn()][$name] = $field;
170 788
        }
171
        foreach ($fieldGroups as $fields) {
172 788
            // We need duplicates only
173 788
            if (count($fields) === 1) {
174
                continue;
175
            }
176
            // Compare
177
            $comparator = new FieldComparator();
178
            foreach ($fields as $name => $field) {
179
                $comparator->addField($name, $field);
180
            }
181
            try {
182
                $comparator->compare();
183
            } catch (Throwable $e) {
184
                throw new Exception\CompilerException(
185
                    sprintf("Error compiling the `%s` role.\n\n%s", $entity->getRole(), $e->getMessage()),
186
                    $e->getCode()
187
                );
188
            }
189
        }
190 788
191 788
        $schema = [];
192 788
        foreach ($entity->getFields() as $name => $field) {
193
            $schema[$name] = $field->getColumn();
194
        }
195 788
196
        return $schema;
197
    }
198 788
199
    private function renderTypecast(Entity $entity): array
200 788
    {
201 788
        $schema = [];
202 788
        foreach ($entity->getFields() as $name => $field) {
203 8
            if ($field->hasTypecast()) {
204
                $schema[$name] = $field->getTypecast();
205
            }
206
        }
207 788
208
        return $schema;
209
    }
210 788
211
    private function renderReferences(Entity $entity): array
212 788
    {
213
        $schema = $entity->getPrimaryFields()->getNames();
214 788
215 788
        foreach ($entity->getFields() as $name => $field) {
216 688
            if ($field->isReferenced()) {
217
                $schema[] = $name;
218
            }
219
        }
220 788
221
        return array_unique($schema);
222
    }
223 784
224
    private function renderRelations(Registry $registry, Entity $entity, array &$schema): void
225 784
    {
226 720
        foreach ($registry->getRelations($entity) as $relation) {
227
            $relation->modifySchema($schema);
228 784
        }
229
    }
230
231
    private function renderTypecastHandler(Entity $entity): array|null|string
232
    {
233
        $defaults = (array) $this->defaults[Schema::TYPECAST_HANDLER] ?? [];
234
235
        if ($defaults === []) {
236
            return $entity->getTypecast();
237
        }
238
239
        // entity with default typecast
240
        if ($entity->getTypecast() === Typecast::class && !\in_array(Typecast::class, $defaults, true)) {
241
            $defaults[] = Typecast::class;
242
        }
243
244
        $typecast = $entity->getTypecast() ?? [];
245
246
        return \array_values(\array_unique(\array_merge(\is_array($typecast) ? $typecast : [$typecast], $defaults)));
247
    }
248
}
249