Passed
Pull Request — main (#133)
by Tom
03:12
created

MetadataFactory::buildMetadataConfigForFields()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 24
nc 5
nop 2
dl 0
loc 43
rs 9.2248
c 0
b 0
f 0
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
18
class MetadataFactory extends AbstractMetadataFactory
19
{
20
    protected EntityManager $entityManager;
21
    protected Config $config;
22
23
    /** @param mixed[] $metadataConfig */
24
    public function __construct(protected ContainerInterface $container, protected array|null $metadataConfig = [])
25
    {
26
        $this->entityManager = $container->get(EntityManager::class);
27
        $this->config        = $container->get(Config::class);
28
    }
29
30
    /** @return mixed[]|null */
31
    public function __invoke(): array|null
32
    {
33
        if ($this->metadataConfig) {
34
            return $this->metadataConfig;
35
        }
36
37
        $entityClasses = [];
38
        foreach ($this->entityManager->getMetadataFactory()->getAllMetadata() as $metadata) {
39
            $entityClasses[] = $metadata->getName();
40
        }
41
42
        if ($this->config->getGlobalEnable()) {
43
            $globalEnable = $this->container->get(GlobalEnable::class);
44
45
            return $globalEnable($entityClasses);
46
        }
47
48
        foreach ($entityClasses as $entityClass) {
49
            $reflectionClass     = new ReflectionClass($entityClass);
50
            $entityClassMetadata = $this->entityManager
51
                ->getMetadataFactory()->getMetadataFor($reflectionClass->getName());
52
53
            $this->buildMetadataConfigForEntity($reflectionClass);
54
            $this->buildMetadataConfigForFields($entityClassMetadata, $reflectionClass);
55
            $this->buildMetadataConfigForAssociations($entityClassMetadata, $reflectionClass);
56
        }
57
58
        return $this->metadataConfig;
59
    }
60
61
    /**
62
     * Using the entity class attributes, generate the metadataConfig.
63
     * The buildMetadataConfig* functions exist to simplify the buildMetadata
64
     * function.
65
     */
66
    private function buildMetadataConfigForEntity(ReflectionClass $reflectionClass): void
67
    {
68
        $entityInstance = null;
69
70
        // Fetch attributes for the entity class filterd by Attribute\Entity
71
        foreach ($reflectionClass->getAttributes(Attribute\Entity::class) as $attribute) {
72
            $instance = $attribute->newInstance();
73
74
            // Only process attributes for the Config group
75
            if ($instance->getGroup() !== $this->config->getGroup()) {
76
                continue;
77
            }
78
79
            // Only one matching instance per group is allowed
80
            assert(
81
                ! $entityInstance,
82
                'Duplicate attribute found for entity '
83
                . $reflectionClass->getName() . ', group ' . $instance->getGroup(),
84
            );
85
            $entityInstance = $instance;
86
87
            // Save entity-level metadata
88
            $this->metadataConfig[$reflectionClass->getName()] = [
89
                'entityClass' => $reflectionClass->getName(),
90
                'byValue' => $this->config->getGlobalByValue() ?? $instance->getByValue(),
91
                'namingStrategy' => $instance->getNamingStrategy(),
92
                'fields' => [],
93
                'filters' => $instance->getFilters(),
94
                'excludeCriteria' => $instance->getExcludeCriteria(),
95
                'description' => $instance->getDescription(),
96
                'typeName' => $instance->getTypeName()
97
                    ? $this->appendGroupSuffix($instance->getTypeName()) :
98
                    $this->getTypeName($reflectionClass->getName()),
99
            ];
100
        }
101
    }
102
103
    private function buildMetadataConfigForFields(
104
        ClassMetadata $entityClassMetadata,
105
        ReflectionClass $reflectionClass,
106
    ): void {
107
        foreach ($entityClassMetadata->getFieldNames() as $fieldName) {
108
            $fieldInstance   = null;
109
            $reflectionField = $reflectionClass->getProperty($fieldName);
110
111
            foreach ($reflectionField->getAttributes(Attribute\Field::class) as $attribute) {
112
                $instance = $attribute->newInstance();
113
114
                // Only process attributes for the same group
115
                if ($instance->getGroup() !== $this->config->getGroup()) {
116
                    continue;
117
                }
118
119
                // Only one matching instance per group is allowed
120
                assert(
121
                    ! $fieldInstance,
122
                    'Duplicate attribute found for field '
123
                    . $fieldName . ', group ' . $instance->getGroup(),
124
                );
125
                $fieldInstance = $instance;
126
127
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['description'] =
128
                    $instance->getDescription();
129
130
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['type'] =
131
                    $instance->getType() ?? $entityClassMetadata->getTypeOfField($fieldName);
132
133
                if ($instance->getStrategy()) {
134
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['strategy'] =
135
                        $instance->getStrategy();
136
137
                    continue;
138
                }
139
140
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['excludeCriteria'] =
141
                    $instance->getExcludeCriteria();
142
143
                // Set default strategy based on field type
144
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$fieldName]['strategy'] =
145
                    $this->getDefaultStrategy($entityClassMetadata->getTypeOfField($fieldName));
146
            }
147
        }
148
    }
149
150
    private function buildMetadataConfigForAssociations(
151
        ClassMetadata $entityClassMetadata,
152
        ReflectionClass $reflectionClass,
153
    ): void {
154
        // Fetch attributes for associations
155
        $associationNames = $this->entityManager->getMetadataFactory()
156
            ->getMetadataFor($reflectionClass->getName())->getAssociationNames();
157
158
        foreach ($associationNames as $associationName) {
159
            $associationInstance   = null;
160
            $reflectionAssociation = $reflectionClass->getProperty($associationName);
161
162
            foreach ($reflectionAssociation->getAttributes(Attribute\Association::class) as $attribute) {
163
                $instance = $attribute->newInstance();
164
165
                // Only process attributes for the same group
166
                if ($instance->getGroup() !== $this->config->getGroup()) {
167
                    continue;
168
                }
169
170
                // Only one matching instance per group is allowed
171
                assert(
172
                    ! $associationInstance,
173
                    'Duplicate attribute found for association '
174
                    . $associationName . ', group ' . $instance->getGroup(),
175
                );
176
                $associationInstance = $instance;
177
178
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['description']             =
179
                    $instance->getDescription();
180
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['excludeCriteria']         =
181
                    $instance->getExcludeCriteria();
182
                $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['filterCriteriaEventName'] =
183
                    $instance->getFilterCriteriaEventName();
184
185
                if ($instance->getStrategy()) {
186
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy']
187
                        = $instance->getStrategy();
188
189
                    continue;
190
                }
191
192
                $mapping = $entityClassMetadata->getAssociationMapping($associationName);
193
194
                // See comment on NullifyOwningAssociation for details of why this is done
195
                if ($mapping['type'] === ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide']) {
196
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy'] =
197
                        Strategy\NullifyOwningAssociation::class;
198
                } else {
199
                    $this->metadataConfig[$reflectionClass->getName()]['fields'][$associationName]['strategy'] =
200
                        Strategy\AssociationDefault::class;
201
                }
202
            }
203
        }
204
    }
205
}
206