Issues (82)

src/Compiler.php (2 issues)

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

107
            $parent = $registry->getEntity(/** @scrutinizer ignore-type */ $schema[Schema::PARENT]);
Loading history...
108
            if ($inheritance->getOuterKey()) {
109 4
                if (!$parent->getFields()->has($inheritance->getOuterKey())) {
110 4
                    throw new WrongParentKeyColumnException($parent, $inheritance->getOuterKey());
111 784
                }
112 6
                $schema[Schema::PARENT_KEY] = $inheritance->getOuterKey();
113
            }
114
        }
115 6
116 6
        $this->renderRelations($registry, $entity, $schema);
117 4
118 2
        if ($registry->hasTable($entity)) {
119
            $schema[Schema::DATABASE] = $registry->getDatabase($entity);
120 2
            $schema[Schema::TABLE] = $registry->getTable($entity);
121
        }
122
123
        // Apply modifiers
124 784
        foreach ($entity->getSchemaModifiers() as $modifier) {
125
            \assert($modifier instanceof SchemaModifierInterface);
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