diff --git a/src/Attributes/Description.php b/src/Attributes/Description.php new file mode 100644 index 0000000..e3496db --- /dev/null +++ b/src/Attributes/Description.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Chevere\Http\Attributes; + +use Attribute; +use Stringable; + +#[Attribute] +class Description implements Stringable +{ + public function __construct( + public readonly string $description = '', + ) { + } + + public function __toString(): string + { + return $this->description; + } +} diff --git a/src/Controller.php b/src/Controller.php index a77f1b6..9cf1791 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -18,6 +18,9 @@ use Chevere\Parameter\Interfaces\ArgumentsInterface; use Chevere\Parameter\Interfaces\ArrayParameterInterface; use Chevere\Parameter\Interfaces\ArrayStringParameterInterface; +use LogicException; +use Throwable; +use function Chevere\Message\message; use function Chevere\Parameter\arguments; use function Chevere\Parameter\arrayp; use function Chevere\Parameter\arrayString; @@ -29,9 +32,9 @@ abstract class Controller extends BaseController implements ControllerInterface private ?ArgumentsInterface $body = null; /** - * @var array + * @var ?array */ - private array $files = []; + private ?array $files = null; public static function acceptQuery(): ArrayStringParameterInterface { @@ -98,13 +101,24 @@ final public function body(): ArgumentsInterface final public function files(): array { - return $this->files; + return $this->files + ??= []; } protected function assertRuntime(): void { - $this->query(); - $this->body(); - $this->files(); + foreach (['query', 'body', 'files'] as $method) { + try { + $this->{$method}(); + } catch (Throwable $e) { + throw new LogicException( + (string) message( + '%topic%: %message%', + topic: $method, + message: $e->getMessage() + ) + ); + } + } } } diff --git a/src/functions.php b/src/functions.php index 4beb5c3..f8dfbaa 100644 --- a/src/functions.php +++ b/src/functions.php @@ -13,10 +13,16 @@ namespace Chevere\Http; +use Chevere\Http\Attributes\Description; use Chevere\Http\Attributes\Request; use Chevere\Http\Attributes\Response; use Chevere\Http\Interfaces\MiddlewaresInterface; use ReflectionClass; +use ReflectionClassConstant; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionProperty; function middlewares(string ...$middleware): MiddlewaresInterface { @@ -32,21 +38,37 @@ function requestAttribute(string $className): Request { // @phpstan-ignore-next-line $reflection = new ReflectionClass($className); - $attributes = $reflection->getAttributes(Request::class); - if ($attributes === []) { - return new (Request::class)(); - } - return $attributes[0]->newInstance(); + // @phpstan-ignore-next-line + return getAttribute($reflection, Request::class); } function responseAttribute(string $className): Response { // @phpstan-ignore-next-line $reflection = new ReflectionClass($className); - $attributes = $reflection->getAttributes(Response::class); + + // @phpstan-ignore-next-line + return getAttribute($reflection, Response::class); +} + +function descriptionAttribute(string $className): Description +{ + // @phpstan-ignore-next-line + $reflection = new ReflectionClass($className); + + // @phpstan-ignore-next-line + return getAttribute($reflection, Description::class); +} + +// @phpstan-ignore-next-line +function getAttribute( + ReflectionClass|ReflectionFunction|ReflectionMethod|ReflectionProperty|ReflectionParameter|ReflectionClassConstant $reflection, + string $attribute +): object { + $attributes = $reflection->getAttributes($attribute); if ($attributes === []) { - return new (Response::class)(); + return new $attribute(); } return $attributes[0]->newInstance(); diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 9087f34..87cbc67 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -18,10 +18,20 @@ use Chevere\Tests\src\AcceptOptionalController; use Chevere\Tests\src\NullController; use InvalidArgumentException; +use LogicException; +use OutOfBoundsException; use PHPUnit\Framework\TestCase; final class ControllerTest extends TestCase { + public function testAssertRuntime(): void + { + $controller = new AcceptController(); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('query: Missing required argument(s): `foo`'); + $controller->getResponse(); + } + public function testDefaults(): void { $controller = new NullController(); @@ -86,27 +96,53 @@ public function testAcceptBodyParametersOptional(): void $this->assertSame('123', $controllerWith->body()->optional('bar')->string()); } - public function testAcceptFileParameters(): void + public function testAcceptFile(): void { $controller = new AcceptController(); - $file = [ + $myFile = [ 'error' => UPLOAD_ERR_OK, 'name' => 'readme.txt', - 'size' => 1313, + 'size' => 12345, 'type' => 'text/plain', 'tmp_name' => '/tmp/file.yx5kVl', ]; + $myImage = [ + 'error' => UPLOAD_ERR_OK, + 'name' => 'image.png', + 'size' => 1234, + 'type' => 'image/png', + 'tmp_name' => '/tmp/file.pnp4t1', + ]; $this->assertSame([], $controller->files()); $controllerWith = $controller->withFiles([ - 'MyFile' => $file, + 'myFile' => $myFile, + 'myImage' => $myImage, ]); $this->assertNotSame($controller, $controllerWith); $this->assertNotEquals($controller, $controllerWith); - $myFile = $controllerWith->files()['MyFile']; - $this->assertSame($file, $myFile->toArray()); + $theFile = $controllerWith->files()['myFile']; + $theImage = $controllerWith->files()['myImage']; + $this->assertSame($myFile, $theFile->toArray()); + $this->assertSame($myImage, $theImage->toArray()); + } + + public function testAcceptFileInvalidArgument(): void + { + $controller = new AcceptController(); $this->expectException(ArgumentCountError::class); + $this->expectExceptionMessage('Missing required argument(s): `error, name, size, type, tmp_name`'); + $controller->withFiles([ + 'myFile' => [], + ]); + } + + public function testAcceptFileMissingKey(): void + { + $controller = new AcceptController(); + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage('Missing key(s) `404`'); $controller->withFiles([ - 'MyFile' => [], + '404' => [], ]); } } diff --git a/tests/src/AcceptController.php b/tests/src/AcceptController.php index 34a861f..48e8c80 100644 --- a/tests/src/AcceptController.php +++ b/tests/src/AcceptController.php @@ -52,11 +52,16 @@ public static function acceptBody(): ArrayParameterInterface public static function acceptFiles(): ArrayParameterInterface { - return arrayp( - MyFile: file( - type: string('/^text\/plain$/') - ) - ); + return + arrayp( + myFile: file( + type: string('/^text\/plain$/') + ) + )->withOptional( + myImage: file( + type: string('/^image\/png$/') + ) + ); } public function run(): array diff --git a/tests/src/AcceptOptionalController.php b/tests/src/AcceptOptionalController.php index d781f7d..0fc1789 100644 --- a/tests/src/AcceptOptionalController.php +++ b/tests/src/AcceptOptionalController.php @@ -48,7 +48,7 @@ public static function acceptBody(): ArrayParameterInterface public static function acceptFiles(): ArrayParameterInterface { return arrayp()->withOptional( - MyFile: file( + myFile: file( type: string('/^text\/plain$/') ) );