MetadataFactory::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

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