Skip to content

Commit 1c2be61

Browse files
committed
Added interfaces support
Version #1
1 parent 88b278c commit 1c2be61

File tree

6 files changed

+184
-50
lines changed

6 files changed

+184
-50
lines changed

src/Annotation/ApiResource.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ final class ApiResource
108108
*/
109109
public $graphql;
110110

111+
/**
112+
* @var bool
113+
*/
114+
public $isInterface;
115+
116+
/**
117+
* @var string
118+
*/
119+
public $implements;
120+
111121
/**
112122
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
113123
*

src/GraphQl/Type/SchemaBuilder.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public function getSchema(): Schema
7272
continue;
7373
}
7474

75+
if ($resourceMetadata->getInterface()) {
76+
continue;
77+
}
78+
7579
if ($resourceMetadata->getGraphqlAttribute($operationName, 'item_query')) {
7680
$queryFields += $this->fieldsBuilder->getItemQueryFields($resourceClass, $resourceMetadata, $operationName, $value);
7781

src/GraphQl/Type/TypeBuilder.php

Lines changed: 144 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
*/
3333
final class TypeBuilder implements TypeBuilderInterface
3434
{
35+
public const INTERFACE_POSTFIX = 'Interface';
36+
public const ITEM_POSTFIX = 'Item';
37+
public const COLLECTION_POSTFIX = 'Collection';
38+
public const DATA_POSTFIX = 'Data';
39+
3540
private $typesContainer;
3641
private $defaultFieldResolver;
3742
private $fieldsBuilderLocator;
@@ -53,6 +58,7 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadata $
5358
if (null !== $mutationName) {
5459
$shortName = $mutationName.ucfirst($shortName);
5560
}
61+
5662
if ($input) {
5763
$shortName .= 'Input';
5864
} elseif (null !== $mutationName) {
@@ -61,70 +67,39 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadata $
6167
}
6268
$shortName .= 'Payload';
6369
}
70+
71+
if ($resourceMetadata->getInterface()) {
72+
$shortName .= self::INTERFACE_POSTFIX;
73+
}
74+
6475
if ('item_query' === $queryName) {
65-
$shortName .= 'Item';
76+
$shortName .= self::ITEM_POSTFIX;
6677
}
78+
6779
if ('collection_query' === $queryName) {
68-
$shortName .= 'Collection';
80+
$shortName .= self::COLLECTION_POSTFIX;
6981
}
82+
7083
if ($wrapped && null !== $mutationName) {
71-
$shortName .= 'Data';
84+
$shortName .= self::DATA_POSTFIX;
7285
}
7386

7487
if ($this->typesContainer->has($shortName)) {
7588
$resourceObjectType = $this->typesContainer->get($shortName);
76-
if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull)) {
89+
if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull || $resourceObjectType instanceof InterfaceType)) {
7790
throw new \UnexpectedValueException(sprintf(
7891
'Expected GraphQL type "%s" to be %s.',
7992
$shortName,
80-
implode('|', [ObjectType::class, NonNull::class])
93+
implode('|', [ObjectType::class, NonNull::class, InterfaceType::class])
8194
));
8295
}
8396

8497
return $resourceObjectType;
8598
}
8699

87-
$ioMetadata = $resourceMetadata->getGraphqlAttribute($mutationName ?? $queryName, $input ? 'input' : 'output', null, true);
88-
if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
89-
$resourceClass = $ioMetadata['class'];
90-
}
91-
92-
$wrapData = !$wrapped && null !== $mutationName && !$input && $depth < 1;
93-
94-
$configuration = [
95-
'name' => $shortName,
96-
'description' => $resourceMetadata->getDescription(),
97-
'resolveField' => $this->defaultFieldResolver,
98-
'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $queryName, $wrapData, $depth, $ioMetadata) {
99-
if ($wrapData) {
100-
$queryNormalizationContext = $resourceMetadata->getGraphqlAttribute($queryName ?? '', 'normalization_context', [], true);
101-
$mutationNormalizationContext = $resourceMetadata->getGraphqlAttribute($mutationName ?? '', 'normalization_context', [], true);
102-
// Use a new type for the wrapped object only if there is a specific normalization context for the mutation.
103-
// If not, use the query type in order to ensure the client cache could be used.
104-
$useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext;
105-
106-
return [
107-
lcfirst($resourceMetadata->getShortName()) => $useWrappedType ?
108-
$this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, true, $depth) :
109-
$this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName ?? 'item_query', null, true, $depth),
110-
'clientMutationId' => GraphQLType::string(),
111-
];
112-
}
113-
114-
$fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
115-
116-
$fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, $depth, $ioMetadata);
117-
118-
if ($input && null !== $mutationName && null !== $mutationArgs = $resourceMetadata->getGraphql()[$mutationName]['args'] ?? null) {
119-
return $fieldsBuilder->resolveResourceArgs($mutationArgs, $mutationName, $resourceMetadata->getShortName()) + ['clientMutationId' => $fields['clientMutationId']];
120-
}
121-
122-
return $fields;
123-
},
124-
'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
125-
];
126-
127-
$resourceObjectType = $input ? GraphQLType::nonNull(new InputObjectType($configuration)) : new ObjectType($configuration);
100+
$resourceObjectType = $resourceMetadata->getInterface()
101+
? $this->buildResourceInterfaceType($resourceClass, $shortName, $resourceMetadata, $input, $queryName, $mutationName, $wrapped, $depth)
102+
: $this->buildResourceObjectType($resourceClass, $shortName, $resourceMetadata, $input, $queryName, $mutationName, $wrapped, $depth);
128103
$this->typesContainer->set($shortName, $resourceObjectType);
129104

130105
return $resourceObjectType;
@@ -227,4 +202,127 @@ public function isCollection(Type $type): bool
227202
{
228203
return $type->isCollection() && Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType();
229204
}
205+
206+
private function buildResourceObjectType(?string $resourceClass, string $shortName, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, bool $wrapped, int $depth)
207+
{
208+
$ioMetadata = $resourceMetadata->getGraphqlAttribute($mutationName ?? $queryName, $input ? 'input' : 'output', null, true);
209+
if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
210+
$resourceClass = $ioMetadata['class'];
211+
}
212+
213+
$wrapData = !$wrapped && null !== $mutationName && !$input && $depth < 1;
214+
$interfaces = ($interface = $resourceMetadata->getImplements())
215+
? $this->getInterfaceTypes($interface)
216+
: [];
217+
218+
$configuration = [
219+
'name' => $shortName,
220+
'description' => $resourceMetadata->getDescription(),
221+
'resolveField' => $this->defaultFieldResolver,
222+
'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $queryName, $wrapData, $depth, $ioMetadata) {
223+
if ($wrapData) {
224+
$queryNormalizationContext = $resourceMetadata->getGraphqlAttribute($queryName ?? '', 'normalization_context', [], true);
225+
$mutationNormalizationContext = $resourceMetadata->getGraphqlAttribute($mutationName ?? '', 'normalization_context', [], true);
226+
// Use a new type for the wrapped object only if there is a specific normalization context for the mutation.
227+
// If not, use the query type in order to ensure the client cache could be used.
228+
$useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext;
229+
230+
return [
231+
lcfirst($resourceMetadata->getShortName()) => $useWrappedType ?
232+
$this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, true, $depth) :
233+
$this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName ?? 'item_query', null, true, $depth),
234+
'clientMutationId' => GraphQLType::string(),
235+
];
236+
}
237+
238+
$fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
239+
240+
$fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, $depth, $ioMetadata);
241+
242+
if ($input && null !== $mutationName && null !== $mutationArgs = $resourceMetadata->getGraphql()[$mutationName]['args'] ?? null) {
243+
return $fieldsBuilder->resolveResourceArgs($mutationArgs, $mutationName, $resourceMetadata->getShortName()) + ['clientMutationId' => $fields['clientMutationId']];
244+
}
245+
246+
return $fields;
247+
},
248+
'interfaces' => $wrapData ? [] : \array_merge([$this->getNodeInterface()], $interfaces),
249+
];
250+
251+
return $input ? GraphQLType::nonNull(new InputObjectType($configuration)) : new ObjectType($configuration);
252+
}
253+
254+
private function buildResourceInterfaceType(?string $resourceClass, string $shortName, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, bool $wrapped, int $depth): ?InterfaceType
255+
{
256+
static $fieldsBuilder;
257+
258+
$ioMetadata = $resourceMetadata->getGraphqlAttribute($mutationName ?? $queryName, $input ? 'input' : 'output', null, true);
259+
if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
260+
$resourceClass = $ioMetadata['class'];
261+
}
262+
263+
$wrapData = !$wrapped && null !== $mutationName && !$input && $depth < 1;
264+
265+
if ($this->typesContainer->has($shortName)) {
266+
$resourceInterface = $this->typesContainer->get($shortName);
267+
if (!$resourceInterface instanceof InterfaceType) {
268+
throw new \UnexpectedValueException(sprintf('Expected GraphQL type "%s" to be %s.', $shortName, InterfaceType::class));
269+
}
270+
271+
return $resourceInterface;
272+
}
273+
274+
$fieldsBuilder = $fieldsBuilder ?? $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
275+
$fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $resourceMetadata, false, $queryName, null, $depth, null);
276+
277+
$resourceInterface = new InterfaceType([
278+
'name' => $shortName,
279+
'description' => $resourceMetadata->getDescription(),
280+
'fields' => $fields,
281+
'resolveType' => function ($value, $context, $info) {
282+
if (!isset($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
283+
throw new \UnexpectedValueException('Resource class was not passed. Interface type can not be used.');
284+
}
285+
286+
$shortName = (new \ReflectionClass($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]))->getShortName().'Item';
287+
288+
if (!$this->typesContainer->has($shortName)) {
289+
throw new \UnexpectedValueException("Type with name $shortName can not be found");
290+
}
291+
292+
$type = $this->typesContainer->get($shortName);
293+
if (!isset($type->config['interfaces'])) {
294+
throw new \UnexpectedValueException("Type \"$shortName\" doesn't implement any interface.");
295+
}
296+
297+
foreach ($type->config['interfaces'] as $interface) {
298+
if ($interface === $info->returnType) {
299+
return $type;
300+
}
301+
}
302+
303+
throw new \UnexpectedValueException("Type \"$type\" must implement interface $info->returnType");
304+
},
305+
]);
306+
307+
$this->typesContainer->set($shortName, $resourceInterface);
308+
309+
return $resourceInterface;
310+
}
311+
312+
private function getInterfaceTypes(string $resourceClass): array
313+
{
314+
try {
315+
$reflection = new \ReflectionClass($resourceClass);
316+
} catch (\ReflectionException $e) {
317+
throw new \UnexpectedValueException("Class $resourceClass can't be found.");
318+
}
319+
320+
$itemTypeName = $reflection->getShortName().self::INTERFACE_POSTFIX.self::ITEM_POSTFIX;
321+
$itemType = $this->typesContainer->has($itemTypeName) ? [$this->typesContainer->get($itemTypeName)] : [];
322+
323+
$collectionTypeName = $reflection->getShortName().self::INTERFACE_POSTFIX.self::COLLECTION_POSTFIX;
324+
$collectionType = $this->typesContainer->has($collectionTypeName) ? [$this->typesContainer->get($collectionTypeName)] : [];
325+
326+
return \array_merge($itemType, $collectionType);
327+
}
230328
}

src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ private function createMetadata(ApiResource $annotation, ResourceMetadata $paren
8787
$annotation->collectionOperations,
8888
$annotation->attributes,
8989
$annotation->subresourceOperations,
90-
$annotation->graphql
90+
$annotation->graphql,
91+
$annotation->isInterface,
92+
$annotation->implements
9193
);
9294
}
9395

src/Metadata/Resource/Factory/AnnotationResourceNameCollectionFactory.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,17 @@ public function create(): ResourceNameCollection
5353
}
5454

5555
foreach (ReflectionClassRecursiveIterator::getReflectionClassesFromDirectories($this->paths) as $className => $reflectionClass) {
56-
if ($this->reader->getClassAnnotation($reflectionClass, ApiResource::class)) {
57-
$classes[$className] = true;
56+
$annotation = $this->reader->getClassAnnotation($reflectionClass, ApiResource::class);
57+
if ($annotation) {
58+
$classes[$className]['enabled'] = true;
59+
$classes[$className]['interface'] = $annotation->isInterface;
5860
}
5961
}
6062

63+
\uasort($classes, static function (array $a, array $b) {
64+
return $b['interface'];
65+
});
66+
6167
return new ResourceNameCollection(array_keys($classes));
6268
}
6369
}

src/Metadata/Resource/ResourceMetadata.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ final class ResourceMetadata
3030
private $subresourceOperations;
3131
private $graphql;
3232
private $attributes;
33+
private $interface;
34+
private $implements;
3335

34-
public function __construct(string $shortName = null, string $description = null, string $iri = null, array $itemOperations = null, array $collectionOperations = null, array $attributes = null, array $subresourceOperations = null, array $graphql = null)
36+
public function __construct(string $shortName = null, string $description = null, string $iri = null, array $itemOperations = null, array $collectionOperations = null, array $attributes = null, array $subresourceOperations = null, array $graphql = null, bool $interface = null, string $implements = null)
3537
{
3638
$this->shortName = $shortName;
3739
$this->description = $description;
@@ -41,6 +43,8 @@ public function __construct(string $shortName = null, string $description = null
4143
$this->subresourceOperations = $subresourceOperations;
4244
$this->graphql = $graphql;
4345
$this->attributes = $attributes;
46+
$this->interface = $interface;
47+
$this->implements = $implements;
4448
}
4549

4650
/**
@@ -89,6 +93,16 @@ public function getIri(): ?string
8993
return $this->iri;
9094
}
9195

96+
public function getInterface(): ?bool
97+
{
98+
return $this->interface;
99+
}
100+
101+
public function getImplements(): ?string
102+
{
103+
return $this->implements;
104+
}
105+
92106
/**
93107
* Returns a new instance with the given IRI.
94108
*/

0 commit comments

Comments
 (0)