Passed
Branch main (2388f5)
by Tom
02:52
created

MetadataFactory::globalEnable()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 61
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 34
c 2
b 0
f 0
nc 10
nop 1
dl 0
loc 61
rs 8.7537

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
92
            // Fetch attributes for associations
93
            $associationNames = $this->entityManager->getMetadataFactory()
94
                ->getMetadataFor($entityClass)->getAssociationNames();
95
96
            foreach ($associationNames as $associationName) {
97
                if (in_array($associationName, $globalIgnore)) {
98
                    continue;
99
                }
100
101
                $this->metadataConfig[$entityClass]['fields'][$associationName]['excludeCriteria'] = [];
102
                $this->metadataConfig[$entityClass]['fields'][$associationName]['description']     = $associationName;
103
104
                // NullifyOwningAssociation is not used for globalEnable
105
                $this->metadataConfig[$entityClass]['fields'][$associationName]['strategy'] =
106
                    Strategy\AssociationDefault::class;
107
            }
108
        }
109
110
        $this->metadata = new Metadata($this->container, $this->metadataConfig);
111
112
        return $this->metadata;
113
    }
114
115
    protected function buildMetadata(): Metadata
116
    {
117
        $entityClasses = [];
118
        foreach ($this->entityManager->getMetadataFactory()->getAllMetadata() as $metadata) {
119
            $entityClasses[] = $metadata->getName();
120
        }
121
122
        if ($this->config->getGlobalEnable()) {
123
            return $this->globalEnable($entityClasses);
124
        }
125
126
        foreach ($entityClasses as $entityClass) {
127
            $reflectionClass     = new ReflectionClass($entityClass);
128
            $entityClassMetadata = $this->entityManager
129
                ->getMetadataFactory()->getMetadataFor($reflectionClass->getName());
130
131
            $this->buildMetadataConfigForEntity($reflectionClass);
132
            $this->buildMetadataConfigForFields($entityClassMetadata, $reflectionClass);
133
            $this->buildMetadataConfigForAssociations($entityClassMetadata, $reflectionClass);
134
        }
135
136
        $this->metadata = new Metadata($this->container, $this->metadataConfig);
137
138
        return $this->metadata;
139
    }
140
141
    /**
142
     * Using the entity class attributes, generate the metadataConfig.
143
     * The buildMetadataConfig* functions exist to simplify the buildMetadata
144
     * function.
145
     */
146
    private function buildMetadataConfigForEntity(ReflectionClass $reflectionClass): void
147
    {
148
        $entityInstance = null;
149
150
        // Fetch attributes for the entity class filterd by Attribute\Entity
151
        foreach ($reflectionClass->getAttributes(Attribute\Entity::class) as $attribute) {
152
            $instance = $attribute->newInstance();
153
154
            // Only process attributes for the Config group
155
            if ($instance->getGroup() !== $this->config->getGroup()) {
156
                continue;
157
            }
158
159
            // Only one matching instance per group is allowed
160
            assert(
161
                ! $entityInstance,
162
                'Duplicate attribute found for entity '
163
                . $reflectionClass->getName() . ', group ' . $instance->getGroup(),
164
            );
165
            $entityInstance = $instance;
166
167
            // Save entity-level metadata
168
            $this->metadataConfig[$reflectionClass->getName()] = [
169
                'entityClass' => $reflectionClass->getName(),
170
                'byValue' => $this->config->getGlobalByValue() ?? $instance->getByValue(),
171
                'namingStrategy' => $instance->getNamingStrategy(),
172
                'fields' => [],
173
                'filters' => $instance->getFilters(),
174
                'excludeCriteria' => $instance->getExcludeCriteria(),
175
                'description' => $instance->getDescription(),
176
                'typeName' => $instance->getTypeName()
177
                    ? $this->appendGroupSuffix($instance->getTypeName()) :
178
                    $this->getTypeName($reflectionClass->getName()),
179
            ];
180
        }
181
    }
182
183
    private function buildMetadataConfigForFields(
184
        ClassMetadata $entityClassMetadata,
185
        ReflectionClass $reflectionClass,
186
    ): void {
187
        foreach ($entityClassMetadata->getFieldNames() as $fieldName) {
188
            $fieldInstance   = null;
189
            $reflectionField = $reflectionClass->getProperty($fieldName);
190
191
            foreach ($reflectionField->getAttributes(Attribute\Field::class) as $attribute) {
192
                $instance = $attribute->newInstance();
193
194
                // Only process attributes for the same group
195
                if ($instance->getGroup() !== $this->config->getGroup()) {
196
                    continue;
197
                }
198
199
                // Only one matching instance per group is allowed
200
                assert(
201
                    ! $fieldInstance,
202
                    'Duplicate attribute found for field '
203
                    . $fieldName . ', group ' . $instance->getGroup(),
204
                );
205
                $fieldInstance = $instance;
206
207
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['description'] =
208
                    $instance->getDescription();
209
210
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['type'] =
211
                    $instance->getType() ?? $entityClassMetadata->getTypeOfField($fieldName);
212
213
                if ($instance->getStrategy()) {
214
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['strategy'] =
215
                        $instance->getStrategy();
216
217
                    continue;
218
                }
219
220
                // Set default strategy based on field type
221
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['strategy'] =
222
                    $this->getDefaultStrategy($entityClassMetadata->getTypeOfField($fieldName));
223
            }
224
        }
225
    }
226
227
    private function buildMetadataConfigForAssociations(
228
        ClassMetadata $entityClassMetadata,
229
        ReflectionClass $reflectionClass,
230
    ): void {
231
        // Fetch attributes for associations
232
        $associationNames = $this->entityManager->getMetadataFactory()
233
            ->getMetadataFor($reflectionClass->getName())->getAssociationNames();
234
235
        foreach ($associationNames as $associationName) {
236
            $associationInstance   = null;
237
            $reflectionAssociation = $reflectionClass->getProperty($associationName);
238
239
            foreach ($reflectionAssociation->getAttributes(Attribute\Association::class) as $attribute) {
240
                $instance = $attribute->newInstance();
241
242
                // Only process attributes for the same group
243
                if ($instance->getGroup() !== $this->config->getGroup()) {
244
                    continue;
245
                }
246
247
                // Only one matching instance per group is allowed
248
                assert(
249
                    ! $associationInstance,
250
                    'Duplicate attribute found for association '
251
                    . $associationName . ', group ' . $instance->getGroup(),
252
                );
253
                $associationInstance = $instance;
254
255
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['description']     =
256
                    $instance->getDescription();
257
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['excludeCriteria'] =
258
                    $instance->getExcludeCriteria();
259
260
                if ($instance->getStrategy()) {
261
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy']
262
                        = $instance->getStrategy();
263
264
                    continue;
265
                }
266
267
                $mapping = $entityClassMetadata->getAssociationMapping($associationName);
268
269
                // See comment on NullifyOwningAssociation for details of why this is done
270
                if ($mapping['type'] === ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide']) {
271
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy'] =
272
                        Strategy\NullifyOwningAssociation::class;
273
                } else {
274
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy'] =
275
                        Strategy\AssociationDefault::class;
276
                }
277
            }
278
        }
279
    }
280
281
    /**
282
     * Strip the configured entityPrefix from the type name
283
     */
284
    private function stripEntityPrefix(string $entityClass): string
285
    {
286
        if ($this->config->getEntityPrefix() !== null) {
287
            if (strpos($entityClass, $this->config->getEntityPrefix()) === 0) {
288
                $entityClass = substr($entityClass, strlen($this->config->getEntityPrefix()));
289
            }
290
        }
291
292
        return str_replace('\\', '_', $entityClass);
293
    }
294
295
    /**
296
     * Append the configured groupSuffix from the type name
297
     */
298
    private function appendGroupSuffix(string $entityClass): string
299
    {
300
        if ($this->config->getGroupSuffix() !== null) {
301
            if ($this->config->getGroupSuffix()) {
302
                $entityClass .= '_' . $this->config->getGroupSuffix();
303
            }
304
        } else {
305
            $entityClass .= '_' . $this->config->getGroup();
306
        }
307
308
        return $entityClass;
309
    }
310
311
    /**
312
     * Compute the GraphQL type name
313
     */
314
    private function getTypeName(string $entityClass): string
315
    {
316
        return $this->appendGroupSuffix($this->stripEntityPrefix($entityClass));
317
    }
318
319
    private function getDefaultStrategy(string|null $fieldType): string
320
    {
321
        // Set default strategy based on field type
322
        /** @psalm-suppress UndefinedDocblockClass */
323
        switch ($fieldType) {
324
            case 'tinyint':
325
            case 'smallint':
326
            case 'integer':
327
            case 'int':
328
                return Strategy\ToInteger::class;
329
330
            case 'boolean':
331
                return Strategy\ToBoolean::class;
332
333
            case 'decimal':
334
            case 'float':
335
                return Strategy\ToFloat::class;
336
337
            case 'bigint':  // bigint is handled as a string internal to php
338
            case 'string':
339
            case 'text':
340
            case 'datetime':
341
            default:
342
                return Strategy\FieldDefault::class;
343
        }
344
    }
345
}
346