diff --git a/src/HttpResponder/HttpController.php b/src/HttpResponder/HttpController.php index d02085290f820328ab633e6f3c77f625c46e3b58..af634d340d1bbc1545bd5bfb468e5c4be0d0b9ec 100644 --- a/src/HttpResponder/HttpController.php +++ b/src/HttpResponder/HttpController.php @@ -80,6 +80,18 @@ abstract readonly class HttpController extends HttpResponder final public function respond(Request $request, Response $response): null|HttpInterceptableInterface|HttpResponderInterface { + if ($this->handleReflection->parameters->isEmpty()) { + /** + * This method is dynamically built and it's checked in the + * constructor. + * + * @psalm-suppress UndefinedMethod + * + * @var null|HttpInterceptableInterface|HttpResponderInterface + */ + return $this->handle(); + } + /** * @var array <string,mixed> */ diff --git a/src/HttpResponderAttributeCollection.php b/src/HttpResponderAttributeCollection.php deleted file mode 100644 index 93ec2112d966b7f3b11abe020e31a3ea1f9966e8..0000000000000000000000000000000000000000 --- a/src/HttpResponderAttributeCollection.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -use Distantmagic\Resonance\Attribute\RespondsToHttp; -use Ds\Set; - -readonly class HttpResponderAttributeCollection -{ - /** - * @var Set<PHPFileReflectionClassAttribute<HttpResponderInterface,RespondsToHttp>> $httpResponderAttributes - */ - public Set $httpResponderAttributes; - - public function __construct() - { - $this->httpResponderAttributes = new Set(); - } -} diff --git a/src/HttpRespondsWithAttributeCollection.php b/src/HttpRespondsWithAttributeCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/OpenAPIMetadataResponseExtractor/CanExtractor.php b/src/OpenAPIMetadataResponseExtractor/CanExtractor.php new file mode 100644 index 0000000000000000000000000000000000000000..487299d2ef11a96b39679d27c6a8b6d964eb7ff7 --- /dev/null +++ b/src/OpenAPIMetadataResponseExtractor/CanExtractor.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\OpenAPIMetadataResponseExtractor; + +use Distantmagic\Resonance\Attribute; +use Distantmagic\Resonance\Attribute\Can; +use Distantmagic\Resonance\Attribute\ExtractsOpenAPIMetadataResponse; +use Distantmagic\Resonance\Attribute\RespondsWith; +use Distantmagic\Resonance\Attribute\Singleton; +use Distantmagic\Resonance\OpenAPIMetadataResponseExtractor; +use Distantmagic\Resonance\OpenAPISchemaResponse; +use Distantmagic\Resonance\RespondsWithAttributeCollection; +use Distantmagic\Resonance\SingletonCollection; +use ReflectionClass; +use RuntimeException; + +/** + * `Can` attribute has `onForbiddenRespondWith` property, which can generate an + * additional type of a resonse. + * + * @template-extends OpenAPIMetadataResponseExtractor<Can> + */ +#[ExtractsOpenAPIMetadataResponse(Can::class)] +#[Singleton(collection: SingletonCollection::OpenAPIMetadataResponseExtractor)] +readonly class CanExtractor extends OpenAPIMetadataResponseExtractor +{ + public function __construct( + private RespondsWithAttributeCollection $respondsWithAttributeCollection, + private RespondsWithExtractor $respondsWithExtractor, + ) {} + + public function extractFromHttpControllerMetadata( + ReflectionClass $reflectionClass, + Attribute $attribute, + ): array { + /** + * @var array<OpenAPISchemaResponse> + */ + $ret = []; + + if (!$attribute->onForbiddenRespondWith) { + return $ret; + } + + $respondsWithAttributes = $this + ->respondsWithAttributeCollection + ->attributes + ->get($attribute->onForbiddenRespondWith, null) + ; + + if (is_null($respondsWithAttributes)) { + throw new RuntimeException(sprintf( + 'You need to add at least one "%s" attribute on "%s"', + RespondsWith::class, + $attribute->onForbiddenRespondWith, + )); + } + + foreach ($respondsWithAttributes as $respondsWithAttribute) { + $extractedResponses = $this + ->respondsWithExtractor + ->extractFromHttpControllerMetadata( + $reflectionClass, + $respondsWithAttribute->attribute, + ) + ; + + foreach ($extractedResponses as $extractedResponse) { + $ret[] = $extractedResponse; + } + } + + return $ret; + } +} diff --git a/src/RespondsToHttpAttributeCollection.php b/src/RespondsToHttpAttributeCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..8b458f10605b71da2dfa90ff059f56b297b32767 --- /dev/null +++ b/src/RespondsToHttpAttributeCollection.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Distantmagic\Resonance\Attribute\RespondsToHttp; +use Ds\Map; + +readonly class RespondsToHttpAttributeCollection +{ + /** + * @var Map<class-string<HttpResponderInterface>, PHPFileReflectionClassAttribute<HttpResponderInterface,RespondsToHttp>> $attributes + */ + public Map $attributes; + + public function __construct() + { + $this->attributes = new Map(); + } +} diff --git a/src/RespondsWithAttributeCollection.php b/src/RespondsWithAttributeCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..4c7db2a66bd31ca4f67ebd8fc7c75fc1d29271d4 --- /dev/null +++ b/src/RespondsWithAttributeCollection.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Distantmagic\Resonance\Attribute\RespondsWith; +use Ds\Map; +use Ds\Set; + +readonly class RespondsWithAttributeCollection +{ + /** + * @var Map<class-string<HttpResponderInterface>, Set<PHPFileReflectionClassAttribute<HttpResponderInterface,RespondsWith>>> $attributes + */ + public Map $attributes; + + public function __construct() + { + $this->attributes = new Map(); + } + + /** + * @param PHPFileReflectionClassAttribute<HttpResponderInterface,RespondsWith> $attribute + */ + public function addAttribute(PHPFileReflectionClassAttribute $attribute): void + { + $className = $attribute->reflectionClass->getName(); + + if (!$this->attributes->hasKey($className)) { + $this->attributes->put($className, new Set()); + } + + $this->attributes->get($className)->add($attribute); + } +} diff --git a/src/SingletonProvider/HttpControllerReflectionMethodCollectionProvider.php b/src/SingletonProvider/HttpControllerReflectionMethodCollectionProvider.php index 1acbe8a1a8eed0acfe3c99fe7bc27f8ae1426482..2e07e57f76d2c8f2f1ca3834f0ee740de5c11028 100644 --- a/src/SingletonProvider/HttpControllerReflectionMethodCollectionProvider.php +++ b/src/SingletonProvider/HttpControllerReflectionMethodCollectionProvider.php @@ -8,8 +8,8 @@ use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\HttpControllerReflectionMethod; use Distantmagic\Resonance\HttpControllerReflectionMethodCollection; use Distantmagic\Resonance\HttpResponder\HttpController; -use Distantmagic\Resonance\HttpResponderAttributeCollection; use Distantmagic\Resonance\PHPProjectFiles; +use Distantmagic\Resonance\RespondsToHttpAttributeCollection; use Distantmagic\Resonance\SingletonContainer; use Distantmagic\Resonance\SingletonProvider; use ReflectionClass; @@ -21,14 +21,14 @@ use ReflectionClass; final readonly class HttpControllerReflectionMethodCollectionProvider extends SingletonProvider { public function __construct( - private HttpResponderAttributeCollection $httpResponderAttributeCollection, + private RespondsToHttpAttributeCollection $respondsToHttpAttributeCollection, ) {} public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): HttpControllerReflectionMethodCollection { $httpControllerMetadataCollection = new HttpControllerReflectionMethodCollection(); - foreach ($this->httpResponderAttributeCollection->httpResponderAttributes as $httpResponderAttribute) { + foreach ($this->respondsToHttpAttributeCollection->attributes as $httpResponderAttribute) { $httpResponderClassName = $httpResponderAttribute->reflectionClass->getName(); if (is_a($httpResponderClassName, HttpController::class, true)) { diff --git a/src/SingletonProvider/OpenAPIPathItemCollectionProvider.php b/src/SingletonProvider/OpenAPIPathItemCollectionProvider.php index ae64f1e832ac6e8965adbd5ae4f87e8b3918ccdb..2ebd86a3352f13c01e4787e2f1bc48b194043009 100644 --- a/src/SingletonProvider/OpenAPIPathItemCollectionProvider.php +++ b/src/SingletonProvider/OpenAPIPathItemCollectionProvider.php @@ -53,11 +53,14 @@ final readonly class OpenAPIPathItemCollectionProvider extends SingletonProvider */ $httpResponderReflectionClass = $belongsToOpenAPISchemaFile->reflectionClass; - $openAPIPathItemCollection->pathItems->add(new OpenAPIPathItem( - $belongsToOpenAPISchemaFile->attribute->schemaSymbol, - $httpResponderReflectionClass, - $respondsToHttpAttribute, - )); + if (OpenAPISchemaSymbol::All !== $belongsToOpenAPISchemaFile->attribute->schemaSymbol) { + $openAPIPathItemCollection->pathItems->add(new OpenAPIPathItem( + $belongsToOpenAPISchemaFile->attribute->schemaSymbol, + $httpResponderReflectionClass, + $respondsToHttpAttribute, + )); + } + $openAPIPathItemCollection->pathItems->add(new OpenAPIPathItem( OpenAPISchemaSymbol::All, $httpResponderReflectionClass, diff --git a/src/SingletonProvider/HttpResponderAttributeCollectionProvider.php b/src/SingletonProvider/RespondsToHttpAttributeCollectionProvider.php similarity index 61% rename from src/SingletonProvider/HttpResponderAttributeCollectionProvider.php rename to src/SingletonProvider/RespondsToHttpAttributeCollectionProvider.php index 38d881b0bdba0011fb1efb34505cd1b021fee000..5315b2104669f95c277aa2586721f34a0d2a338c 100644 --- a/src/SingletonProvider/HttpResponderAttributeCollectionProvider.php +++ b/src/SingletonProvider/RespondsToHttpAttributeCollectionProvider.php @@ -6,29 +6,32 @@ namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\Attribute\RespondsToHttp; use Distantmagic\Resonance\Attribute\Singleton; -use Distantmagic\Resonance\HttpResponderAttributeCollection; use Distantmagic\Resonance\HttpResponderCollection; use Distantmagic\Resonance\HttpResponderInterface; use Distantmagic\Resonance\PHPFileReflectionClassAttribute; use Distantmagic\Resonance\PHPProjectFiles; +use Distantmagic\Resonance\RespondsToHttpAttributeCollection; use Distantmagic\Resonance\SingletonContainer; use Distantmagic\Resonance\SingletonProvider; /** * @template-extends SingletonProvider<HttpResponderCollection> */ -#[Singleton(provides: HttpResponderAttributeCollection::class)] -final readonly class HttpResponderAttributeCollectionProvider extends SingletonProvider +#[Singleton(provides: RespondsToHttpAttributeCollection::class)] +final readonly class RespondsToHttpAttributeCollectionProvider extends SingletonProvider { - public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): HttpResponderAttributeCollection + public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): RespondsToHttpAttributeCollection { - $httpResponderAttributeCollection = new HttpResponderAttributeCollection(); + $httpResponderAttributeCollection = new RespondsToHttpAttributeCollection(); /** * @var PHPFileReflectionClassAttribute<HttpResponderInterface,RespondsToHttp> */ - foreach ($phpProjectFiles->findByAttribute(RespondsToHttp::class) as $responderAttribute) { - $httpResponderAttributeCollection->httpResponderAttributes->add($responderAttribute); + foreach ($phpProjectFiles->findByAttribute(RespondsToHttp::class) as $attribute) { + $httpResponderAttributeCollection->attributes->put( + $attribute->reflectionClass->getName(), + $attribute + ); } return $httpResponderAttributeCollection; diff --git a/src/SingletonProvider/RespondsWithAttributeCollectionProvider.php b/src/SingletonProvider/RespondsWithAttributeCollectionProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..597493b8cd920b76fb29924af81630cc455c4078 --- /dev/null +++ b/src/SingletonProvider/RespondsWithAttributeCollectionProvider.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\SingletonProvider; + +use Distantmagic\Resonance\Attribute\RespondsWith; +use Distantmagic\Resonance\Attribute\Singleton; +use Distantmagic\Resonance\HttpResponderCollection; +use Distantmagic\Resonance\HttpResponderInterface; +use Distantmagic\Resonance\PHPFileReflectionClassAttribute; +use Distantmagic\Resonance\PHPProjectFiles; +use Distantmagic\Resonance\RespondsWithAttributeCollection; +use Distantmagic\Resonance\SingletonContainer; +use Distantmagic\Resonance\SingletonProvider; + +/** + * @template-extends SingletonProvider<HttpResponderCollection> + */ +#[Singleton(provides: RespondsWithAttributeCollection::class)] +final readonly class RespondsWithAttributeCollectionProvider extends SingletonProvider +{ + public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): RespondsWithAttributeCollection + { + $respondsWithAttributeCollection = new RespondsWithAttributeCollection(); + + /** + * @var PHPFileReflectionClassAttribute<HttpResponderInterface,RespondsWith> + */ + foreach ($phpProjectFiles->findByAttribute(RespondsWith::class) as $attribute) { + $respondsWithAttributeCollection->addAttribute($attribute); + } + + return $respondsWithAttributeCollection; + } +}