Passed
Push — main ( 55ccc6...c00776 )
by Tom
01:12 queued 13s
created

MetadataFactory::getDefaultStrategy()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 13
c 2
b 0
f 0
nc 8
nop 1
dl 0
loc 19
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ApiSkeletons\Doctrine\GraphQL\Metadata;
6
7
use ApiSkeletons\Doctrine\GraphQL\Attribute;
8
use ApiSkeletons\Doctrine\GraphQL\Config;
9
use ApiSkeletons\Doctrine\GraphQL\Hydrator\Strategy;
10
use Doctrine\ORM\EntityManager;
11
use Doctrine\ORM\Mapping\ClassMetadata;
12
use Doctrine\ORM\Mapping\ClassMetadataInfo;
13
use Psr\Container\ContainerInterface;
14
use ReflectionClass;
15
16
use function assert;
17
use function in_array;
18
use function str_replace;
19
use function strlen;
20
use function strpos;
21
use function substr;
22
23
class MetadataFactory
24
{
25
    protected Metadata|null $metadata = null;
26
    protected EntityManager $entityManager;
27
    protected Config $config;
28
29
    /** @param mixed|null $metadataConfig */
30
    public function __construct(protected ContainerInterface $container, protected array|null $metadataConfig)
31
    {
32
        $this->entityManager = $container->get(EntityManager::class);
33
        $this->config        = $container->get(Config::class);
34
35
        if (empty($metadataConfig)) {
36
            return;
37
        }
38
39
        $this->metadata = new Metadata($this->container, $metadataConfig);
40
    }
41
42
    public function getMetadata(): Metadata
43
    {
44
        if ($this->metadata) {
45
            return $this->metadata;
46
        }
47
48
        return $this->buildMetadata();
49
    }
50
51
    /** @param string[] $entityClasses */
52
    private function globalEnable(array $entityClasses): Metadata
53
    {
54
        $globalIgnore = $this->config->getGlobalIgnore();
55
56
        foreach ($entityClasses as $entityClass) {
57
            // Get extract by value or reference
58
            $byValue = $this->config->getGlobalByValue() ?? true;
59
60
            // Save entity-level metadata
61
            $this->metadataConfig[$entityClass] = [
62
                'entityClass' => $entityClass,
63
                'byValue' => $byValue,
64
                'namingStrategy' => null,
65
                'fields' => [],
66
                'filters' => [],
67
                'excludeCriteria' => [],
68
                'description' => $entityClass,
69
                'typeName' => $this->getTypeName($entityClass),
70
            ];
71
72
            // Fetch fields
73
            $entityClassMetadata = $this->entityManager->getMetadataFactory()->getMetadataFor($entityClass);
74
            $fieldNames          = $entityClassMetadata->getFieldNames();
75
76
            foreach ($fieldNames as $fieldName) {
77
                if (in_array($fieldName, $globalIgnore)) {
78
                    continue;
79
                }
80
81
                $this->metadataConfig[$entityClass]['fields'][$fieldName]['description'] =
82
                    $fieldName;
83
84
                $this->metadataConfig[$entityClass]['fields'][$fieldName]['type'] =
85
                    $entityClassMetadata->getTypeOfField($fieldName);
86
87
                // Set default strategy based on field type
88
                $this->metadataConfig[$entityClass]['fields'][$fieldName]['strategy'] =
89
                    $this->getDefaultStrategy($entityClassMetadata->getTypeOfField($fieldName));
90
91
                $this->metadataConfig[$entityClass]['fields'][$fieldName]['excludeCriteria'] = [];
92
            }
93
94
            // Fetch attributes for associations
95
            $associationNames = $this->entityManager->getMetadataFactory()
96
                ->getMetadataFor($entityClass)->getAssociationNames();
97
98
            foreach ($associationNames as $associationName) {
99
                if (in_array($associationName, $globalIgnore)) {
100
                    continue;
101
                }
102
103
                $this->metadataConfig[$entityClass]['fields'][$associationName]['excludeCriteria'] = [];
104
                $this->metadataConfig[$entityClass]['fields'][$associationName]['description']     = $associationName;
105
106
                // NullifyOwningAssociation is not used for globalEnable
107
                $this->metadataConfig[$entityClass]['fields'][$associationName]['strategy'] =
108
                    Strategy\AssociationDefault::class;
109
            }
110
        }
111
112
        $this->metadata = new Metadata($this->container, $this->metadataConfig);
113
114
        return $this->metadata;
115
    }
116
117
    protected function buildMetadata(): Metadata
118
    {
119
        $entityClasses = [];
120
        foreach ($this->entityManager->getMetadataFactory()->getAllMetadata() as $metadata) {
121
            $entityClasses[] = $metadata->getName();
122
        }
123
124
        if ($this->config->getGlobalEnable()) {
125
            return $this->globalEnable($entityClasses);
126
        }
127
128
        foreach ($entityClasses as $entityClass) {
129
            $reflectionClass     = new ReflectionClass($entityClass);
130
            $entityClassMetadata = $this->entityManager
131
                ->getMetadataFactory()->getMetadataFor($reflectionClass->getName());
132
133
            $this->buildMetadataConfigForEntity($reflectionClass);
134
            $this->buildMetadataConfigForFields($entityClassMetadata, $reflectionClass);
135
            $this->buildMetadataConfigForAssociations($entityClassMetadata, $reflectionClass);
136
        }
137
138
        $this->metadata = new Metadata($this->container, $this->metadataConfig);
139
140
        return $this->metadata;
141
    }
142
143
    /**
144
     * Using the entity class attributes, generate the metadataConfig.
145
     * The buildMetadataConfig* functions exist to simplify the buildMetadata
146
     * function.
147
     */
148
    private function buildMetadataConfigForEntity(ReflectionClass $reflectionClass): void
149
    {
150
        $entityInstance = null;
151
152
        // Fetch attributes for the entity class filterd by Attribute\Entity
153
        foreach ($reflectionClass->getAttributes(Attribute\Entity::class) as $attribute) {
154
            $instance = $attribute->newInstance();
155
156
            // Only process attributes for the Config group
157
            if ($instance->getGroup() !== $this->config->getGroup()) {
158
                continue;
159
            }
160
161
            // Only one matching instance per group is allowed
162
            assert(
163
                ! $entityInstance,
164
                'Duplicate attribute found for entity '
165
                . $reflectionClass->getName() . ', group ' . $instance->getGroup(),
166
            );
167
            $entityInstance = $instance;
168
169
            // Save entity-level metadata
170
            $this->metadataConfig[$reflectionClass->getName()] = [
171
                'entityClass' => $reflectionClass->getName(),
172
                'byValue' => $this->config->getGlobalByValue() ?? $instance->getByValue(),
173
                'namingStrategy' => $instance->getNamingStrategy(),
174
                'fields' => [],
175
                'filters' => $instance->getFilters(),
176
                'excludeCriteria' => $instance->getExcludeCriteria(),
177
                'description' => $instance->getDescription(),
178
                'typeName' => $instance->getTypeName()
179
                    ? $this->appendGroupSuffix($instance->getTypeName()) :
180
                    $this->getTypeName($reflectionClass->getName()),
181
            ];
182
        }
183
    }
184
185
    private function buildMetadataConfigForFields(
186
        ClassMetadata $entityClassMetadata,
187
        ReflectionClass $reflectionClass,
188
    ): void {
189
        foreach ($entityClassMetadata->getFieldNames() as $fieldName) {
190
            $fieldInstance   = null;
191
            $reflectionField = $reflectionClass->getProperty($fieldName);
192
193
            foreach ($reflectionField->getAttributes(Attribute\Field::class) as $attribute) {
194
                $instance = $attribute->newInstance();
195
196
                // Only process attributes for the same group
197
                if ($instance->getGroup() !== $this->config->getGroup()) {
198
                    continue;
199
                }
200
201
                // Only one matching instance per group is allowed
202
                assert(
203
                    ! $fieldInstance,
204
                    'Duplicate attribute found for field '
205
                    . $fieldName . ', group ' . $instance->getGroup(),
206
                );
207
                $fieldInstance = $instance;
208
209
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['description'] =
210
                    $instance->getDescription();
211
212
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['type'] =
213
                    $instance->getType() ?? $entityClassMetadata->getTypeOfField($fieldName);
214
215
                if ($instance->getStrategy()) {
216
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['strategy'] =
217
                        $instance->getStrategy();
218
219
                    continue;
220
                }
221
222
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['excludeCriteria'] =
223
                    $instance->getExcludeCriteria();
224
225
                // Set default strategy based on field type
226
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['strategy'] =
227
                    $this->getDefaultStrategy($entityClassMetadata->getTypeOfField($fieldName));
228
            }
229
        }
230
    }
231
232
    private function buildMetadataConfigForAssociations(
233
        ClassMetadata $entityClassMetadata,
234
        ReflectionClass $reflectionClass,
235
    ): void {
236
        // Fetch attributes for associations
237
        $associationNames = $this->entityManager->getMetadataFactory()
238
            ->getMetadataFor($reflectionClass->getName())->getAssociationNames();
239
240
        foreach ($associationNames as $associationName) {
241
            $associationInstance   = null;
242
            $reflectionAssociation = $reflectionClass->getProperty($associationName);
243
244
            foreach ($reflectionAssociation->getAttributes(Attribute\Association::class) as $attribute) {
245
                $instance = $attribute->newInstance();
246
247
                // Only process attributes for the same group
248
                if ($instance->getGroup() !== $this->config->getGroup()) {
249
                    continue;
250
                }
251
252
                // Only one matching instance per group is allowed
253
                assert(
254
                    ! $associationInstance,
255
                    'Duplicate attribute found for association '
256
                    . $associationName . ', group ' . $instance->getGroup(),
257
                );
258
                $associationInstance = $instance;
259
260
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['description']     =
261
                    $instance->getDescription();
262
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['excludeCriteria'] =
263
                    $instance->getExcludeCriteria();
264
265
                if ($instance->getStrategy()) {
266
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy']
267
                        = $instance->getStrategy();
268
269
                    continue;
270
                }
271
272
                $mapping = $entityClassMetadata->getAssociationMapping($associationName);
273
274
                // See comment on NullifyOwningAssociation for details of why this is done
275
                if ($mapping['type'] === ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide']) {
276
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy'] =
277
                        Strategy\NullifyOwningAssociation::class;
278
                } else {
279
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy'] =
280
                        Strategy\AssociationDefault::class;
281
                }
282
            }
283
        }
284
    }
285
286
    /**
287
     * Strip the configured entityPrefix from the type name
288
     */
289
    private function stripEntityPrefix(string $entityClass): string
290
    {
291
        if ($this->config->getEntityPrefix() !== null) {
292
            if (strpos($entityClass, $this->config->getEntityPrefix()) === 0) {
293
                $entityClass = substr($entityClass, strlen($this->config->getEntityPrefix()));
294
            }
295
        }
296
297
        return str_replace('\\', '_', $entityClass);
298
    }
299
300
    /**
301
     * Append the configured groupSuffix from the type name
302
     */
303
    private function appendGroupSuffix(string $entityClass): string
304
    {
305
        if ($this->config->getGroupSuffix() !== null) {
306
            if ($this->config->getGroupSuffix()) {
307
                $entityClass .= '_' . $this->config->getGroupSuffix();
308
            }
309
        } else {
310
            $entityClass .= '_' . $this->config->getGroup();
311
        }
312
313
        return $entityClass;
314
    }
315
316
    /**
317
     * Compute the GraphQL type name
318
     */
319
    private function getTypeName(string $entityClass): string
320
    {
321
        return $this->appendGroupSuffix($this->stripEntityPrefix($entityClass));
322
    }
323
324
    private function getDefaultStrategy(string|null $fieldType): string
325
    {
326
        // Set default strategy based on field type
327
        switch ($fieldType) {
328
            case 'tinyint':
329
            case 'smallint':
330
            case 'integer':
331
            case 'int':
332
                return Strategy\ToInteger::class;
333
334
            case 'boolean':
335
                return Strategy\ToBoolean::class;
336
337
            case 'decimal':
338
            case 'float':
339
                return Strategy\ToFloat::class;
340
341
            default:
342
                return Strategy\FieldDefault::class;
343
        }
344
    }
345
}
346