55namespace Sitegeist \SchemeOnYou \Domain \Schema ;
66
77use Neos \Flow \Annotations as Flow ;
8+ use Neos \Flow \Core \Bootstrap ;
9+ use Neos \Flow \Reflection \ReflectionService ;
810use Sitegeist \SchemeOnYou \Domain \Metadata \Schema as SchemaMetadata ;
911use Sitegeist \SchemeOnYou \Domain \Metadata \StringProperty ;
12+ use Sitegeist \SchemeOnYou \Infrastructure \InterfaceImplementationDetector ;
1013
1114#[Flow \Proxy(false )]
1215final readonly class OpenApiSchema implements \JsonSerializable
@@ -27,22 +30,29 @@ public function __construct(
2730 public ?array $ required = null ,
2831 public ?string $ format = null ,
2932 public ?OpenApiReference $ items = null ,
33+ public ?OpenApiSchemaOrReferenceCollection $ oneOf = null ,
34+ public ?OpenApiSchemaOrReferenceCollection $ anyOf = null ,
35+ public ?OpenApiSchemaOrReferenceCollection $ allOf = null ,
36+ public ?OpenApiSchemaDiscriminator $ discriminator = null ,
3037 ) {
3138 }
3239
3340 /**
34- * @phpstan-param class- string $className
41+ * @phpstan-param string $typeName
3542 */
36- public static function fromClassName (string $ className ): self
43+ public static function fromTypeName (string $ typeName ): self
3744 {
38- if (enum_exists ($ className )) {
39- return self ::fromReflectionEnum (new \ReflectionEnum ($ className ));
40- } elseif (class_exists ($ className )) {
41- return self ::fromReflectionClass (new \ReflectionClass ($ className ));
45+ if (enum_exists ($ typeName )) {
46+ return self ::fromReflectionEnum (new \ReflectionEnum ($ typeName ));
47+ } elseif (class_exists ($ typeName )) {
48+ return self ::fromReflectionClass (new \ReflectionClass ($ typeName ));
49+ } elseif (interface_exists ($ typeName )) {
50+ return self ::fromInterfaceReflectionClass (new \ReflectionClass ($ typeName ));
4251 }
43- throw new \DomainException ('Cannot create definition from incomprehensible type ' . $ className , 1709500131 );
52+ throw new \DomainException ('Cannot create definition from incomprehensible type ' . $ typeName , 1709500131 );
4453 }
4554
55+
4656 private static function fromReflectionEnum (\ReflectionEnum $ reflection ): self
4757 {
4858 $ definitionMetadata = SchemaMetadata::fromReflectionClass ($ reflection );
@@ -103,13 +113,10 @@ public static function fromReflectionParameter(\ReflectionParameter $reflection)
103113 type: 'string ' ,
104114 format: 'duration ' ,
105115 );
106- } elseif (class_exists ($ typeName )) {
107- return self ::fromClassName ($ typeName );
108- } else {
109- throw new \DomainException (sprintf ('Schema can only be created for collection, value objects and backed enums "%s" is neither. ' , $ reflection ->getName ()));
110116 }
117+ return self ::fromReflectionNamedType ($ reflectionType );
111118 } elseif ($ reflectionType instanceof \ReflectionUnionType) {
112- throw new \ DomainException ( sprintf ( ' Schema can only be created for collection, value objects and backed enums "%s" is neither. ' , $ reflection -> getName ()) );
119+ return self :: fromReflectionUnionType ( $ reflectionType );
113120 } else {
114121 throw new \DomainException (sprintf ('Schema can only be created for collection, value objects and backed enums "%s" is neither. ' , $ reflection ->getName ()));
115122 }
@@ -136,6 +143,9 @@ public static function fromReflectionClass(\ReflectionClass $reflection): self
136143 return self ::fromCollectionReflectionClass ($ reflection );
137144 } elseif (IsDataTransferObject::isSatisfiedByReflectionClass ($ reflection )) {
138145 return self ::fromObjectReflectionClass ($ reflection );
146+ } elseif (interface_exists ($ reflection ->getName ())) {
147+ // @todo is this still used
148+ return self ::fromInterfaceReflectionClass ($ reflection );
139149 }
140150 throw new \DomainException (sprintf ('Schema can only be created for collection, value objects and backed enums "%s" is neither. ' , $ reflection ->getName ()));
141151 }
@@ -215,26 +225,10 @@ private static function fromObjectReflectionClass(\ReflectionClass $reflectionCl
215225 $ type ,
216226 $ reflectionParameter
217227 ),
218- \ReflectionUnionType::class => [
219- 'oneOf ' => array_map (
220- fn (\ReflectionType $ singleType ): SchemaType |OpenApiReference
221- => match (get_class ($ singleType )) {
222- \ReflectionIntersectionType::class,
223- => throw new \DomainException (
224- 'Cannot resolve schema reference from intersection type '
225- . ' given for constructor parameter '
226- . $ reflectionParameter ->name . ' of class ' . $ reflectionClass ->name ,
227- 1709560366
228- ),
229- \ReflectionNamedType::class => SchemaType::selfOrReferenceFromReflectionNamedType (
230- $ singleType ,
231- $ reflectionParameter ,
232- ),
233- default => throw new \DomainException ('wat ' )
234- },
235- $ type ->getTypes ()
236- )
237- ],
228+ \ReflectionUnionType::class => SchemaType::fromReflectionUnionType (
229+ $ type ,
230+ $ reflectionParameter
231+ ),
238232 \ReflectionIntersectionType::class => throw new \DomainException (
239233 'Cannot resolve schema reference from intersection type given for constructor parameter '
240234 . $ reflectionParameter ->name . ' of class ' . $ reflectionClass ->name ,
@@ -263,6 +257,81 @@ private static function fromObjectReflectionClass(\ReflectionClass $reflectionCl
263257 );
264258 }
265259
260+ private static function fromReflectionNamedType (\ReflectionNamedType $ reflectionType ): self
261+ {
262+ return self ::fromTypeName ($ reflectionType ->getName ());
263+ }
264+
265+ private static function fromReflectionUnionType (\ReflectionUnionType $ reflection ): self
266+ {
267+ $ subSchemas = [];
268+ foreach ($ reflection ->getTypes () as $ type ) {
269+ if ($ type instanceof \ReflectionNamedType) {
270+ $ subSchemas [] = new OpenApiSchema (
271+ type: 'object ' ,
272+ allOf: new OpenApiSchemaOrReferenceCollection (
273+ self ::discriminatorForClassName ($ type ->getName ()),
274+ OpenApiReference::fromClassName ($ type ->getName ())
275+ )
276+ );
277+ } else {
278+ throw new \DomainException ('Union types are only supported for named types. ' . get_class ($ type ) . ' given ' );
279+ }
280+ }
281+
282+ return new self (
283+ type: 'object ' ,
284+ oneOf: new OpenApiSchemaOrReferenceCollection (...$ subSchemas ),
285+ discriminator: new OpenApiSchemaDiscriminator ()
286+ );
287+ }
288+
289+ /**
290+ * @param \ReflectionClass<object> $reflectionClass
291+ */
292+ private static function fromInterfaceReflectionClass (\ReflectionClass $ reflectionClass ): self
293+ {
294+ $ schemaMetadata = SchemaMetadata::fromReflectionClass ($ reflectionClass );
295+
296+ $ detector = new InterfaceImplementationDetector ();
297+ $ implementationClasses = $ detector ->detect ($ reflectionClass ->name );
298+
299+ $ implementationSchemas = [];
300+ foreach ($ implementationClasses as $ implementationClass ) {
301+ $ implementationSchemas [] = new OpenApiSchema (
302+ type: 'object ' ,
303+ allOf: new OpenApiSchemaOrReferenceCollection (
304+ self ::discriminatorForClassName ($ implementationClass ),
305+ OpenApiReference::fromClassName ($ implementationClass )
306+ )
307+ );
308+ }
309+
310+ return new self (
311+ type: 'object ' ,
312+ name: $ schemaMetadata ->name ?: $ reflectionClass ->getShortName (),
313+ description: $ schemaMetadata ->description ,
314+ oneOf: new OpenApiSchemaOrReferenceCollection (...$ implementationSchemas ),
315+ discriminator: new OpenApiSchemaDiscriminator ()
316+ );
317+ }
318+
319+ public static function discriminatorForClassName (string $ className ): self
320+ {
321+ return new self (
322+ type: 'object ' ,
323+ properties: [
324+ OpenApiSchemaDiscriminator::DISCRIMINATOR_NAME => new SchemaType (
325+ [
326+ 'type ' => 'string ' ,
327+ 'enum ' => [str_replace ('\\' , '_ ' , $ className )]
328+ ]
329+ )
330+ ],
331+ required: [OpenApiSchemaDiscriminator::DISCRIMINATOR_NAME ]
332+ );
333+ }
334+
266335 public function toReference (): OpenApiReference
267336 {
268337 return new OpenApiReference ('#/components/schemas/ ' . $ this ->name );
0 commit comments