Passed
Push — 2.x ( b60830...92ccd4 )
by Aleksei
13:13
created

Compiler::renderTypecastHandler()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 14
rs 10
ccs 5
cts 5
cp 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Schema;
6
7
use Cycle\ORM\SchemaInterface as Schema;
8
use Cycle\Schema\Definition\Comparator\FieldComparator;
9
use Cycle\Schema\Definition\Entity;
10
use Cycle\Schema\Definition\Field;
11
use Cycle\Schema\Definition\Inheritance\JoinedTable;
12
use Cycle\Schema\Definition\Inheritance\SingleTable;
13
use Cycle\Schema\Exception\CompilerException;
14
use Cycle\Schema\Exception\SchemaModifierException;
15
use Cycle\Schema\Exception\TableInheritance\DiscriminatorColumnNotPresentException;
16
use Cycle\Schema\Exception\TableInheritance\WrongDiscriminatorColumnException;
17
use Cycle\Schema\Exception\TableInheritance\WrongParentKeyColumnException;
18
use Throwable;
19
20
final class Compiler
21
{
22
    /** @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...
23
    private array $result = [];
24
25
    /**
26
     * Compile the registry schema.
27
     *
28
     * @param GeneratorInterface[] $generators
29
     */
30
    public function compile(Registry $registry, array $generators = [], array $defaults = []): array
31
    {
32
        $registry->getDefaults()->merge($defaults);
33
34
        foreach ($generators as $generator) {
35
            if (!$generator instanceof GeneratorInterface) {
36
                throw new CompilerException(
37
                    sprintf(
38
                        'Invalid generator `%s`. It should implement `%s` interface.',
39
                        \is_object($generator) ? $generator::class : \var_export($generator, true),
40
                        GeneratorInterface::class
41
                    )
42 918
                );
43
            }
44 918
45
            $registry = $generator->run($registry);
46 918
        }
47 654
48 2
        foreach ($registry->getIterator() as $entity) {
49 2
            if ($entity->hasPrimaryKey() || $entity->isChildOfSingleTableInheritance()) {
50 2
                $this->compute($registry, $entity);
51 2
            }
52 2
        }
53
54
        return $this->result;
55
    }
56
57 652
    /**
58
     * Get compiled schema result.
59
     */
60 788
    public function getSchema(): array
61 788
    {
62 788
        return $this->result;
63
    }
64
65
    /**
66 780
     * Compile entity and relation definitions into packed ORM schema.
67
     */
68
    private function compute(Registry $registry, Entity $entity): void
69
    {
70
        $defaults = $registry->getDefaults();
71
72 16
        $schema = [
73
            Schema::ENTITY => $entity->getClass(),
74 16
            Schema::SOURCE => $entity->getSource() ?? $defaults[Schema::SOURCE],
75
            Schema::MAPPER => $entity->getMapper() ?? $defaults[Schema::MAPPER],
76
            Schema::REPOSITORY => $entity->getRepository() ?? $defaults[Schema::REPOSITORY],
77
            Schema::SCOPE => $entity->getScope() ?? $defaults[Schema::SCOPE],
78
            Schema::SCHEMA => $entity->getSchema(),
79
            Schema::TYPECAST_HANDLER => $this->renderTypecastHandler($registry->getDefaults(), $entity),
80 788
            Schema::PRIMARY_KEY => $entity->getPrimaryFields()->getNames(),
81
            Schema::COLUMNS => $this->renderColumns($entity),
82 788
            Schema::FIND_BY_KEYS => $this->renderReferences($entity),
83 788
            Schema::TYPECAST => $this->renderTypecast($entity),
84 788
            Schema::RELATIONS => [],
85 788
        ];
86 788
87 788
        // For table inheritance we need to fill specific schema segments
88 788
        $inheritance = $entity->getInheritance();
89 788
        if ($inheritance instanceof SingleTable) {
90 788
            // Check if discriminator column defined and is not null or empty
91 788
            $discriminator = $inheritance->getDiscriminator();
92 788
            if ($discriminator === null || $discriminator === '') {
93 788
                throw new DiscriminatorColumnNotPresentException($entity);
94 788
            }
95
            if (!$entity->getFields()->has($discriminator)) {
96
                throw new WrongDiscriminatorColumnException($entity, $discriminator);
97
            }
98 788
99 788
            $schema[Schema::CHILDREN] = $inheritance->getChildren();
100
            $schema[Schema::DISCRIMINATOR] = $discriminator;
101 8
        } elseif ($inheritance instanceof JoinedTable) {
102 8
            $schema[Schema::PARENT] = $inheritance->getParent()->getRole();
103 2
            assert($schema[Schema::PARENT] !== null);
104
105 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

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