Completed
Push — 2.x ( 67da7e...8fbc46 )
by Aleksei
40s queued 38s
created

Compiler::renderGeneratedFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 3
rs 10
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
        $role = $entity->getRole();
72 16
        \assert($role !== null);
73
74 16
        $schema = [
75
            Schema::ENTITY => $entity->getClass(),
76
            Schema::SOURCE => $entity->getSource() ?? $defaults[Schema::SOURCE],
77
            Schema::MAPPER => $entity->getMapper() ?? $defaults[Schema::MAPPER],
78
            Schema::REPOSITORY => $entity->getRepository() ?? $defaults[Schema::REPOSITORY],
79
            Schema::SCOPE => $entity->getScope() ?? $defaults[Schema::SCOPE],
80 788
            Schema::SCHEMA => $entity->getSchema(),
81
            Schema::TYPECAST_HANDLER => $this->renderTypecastHandler($registry->getDefaults(), $entity),
82 788
            Schema::PRIMARY_KEY => $entity->getPrimaryFields()->getNames(),
83 788
            Schema::COLUMNS => $this->renderColumns($entity),
84 788
            Schema::FIND_BY_KEYS => $this->renderReferences($entity),
85 788
            Schema::TYPECAST => $this->renderTypecast($entity),
86 788
            Schema::RELATIONS => [],
87 788
            Schema::GENERATED_FIELDS => $this->renderGeneratedFields($entity),
0 ignored issues
show
Bug introduced by
The constant Cycle\ORM\SchemaInterface::GENERATED_FIELDS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
88 788
        ];
89 788
90 788
        // For table inheritance we need to fill specific schema segments
91 788
        $inheritance = $entity->getInheritance();
92 788
        if ($inheritance instanceof SingleTable) {
93 788
            // Check if discriminator column defined and is not null or empty
94 788
            $discriminator = $inheritance->getDiscriminator();
95
            if ($discriminator === null || $discriminator === '') {
96
                throw new DiscriminatorColumnNotPresentException($entity);
97
            }
98 788
            if (!$entity->getFields()->has($discriminator)) {
99 788
                throw new WrongDiscriminatorColumnException($entity, $discriminator);
100
            }
101 8
102 8
            $schema[Schema::CHILDREN] = $inheritance->getChildren();
103 2
            $schema[Schema::DISCRIMINATOR] = $discriminator;
104
        } elseif ($inheritance instanceof JoinedTable) {
105 6
            $schema[Schema::PARENT] = $inheritance->getParent()->getRole();
106 2
            assert($schema[Schema::PARENT] !== null);
107
108
            $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

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