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