|
| 1 | +# API Controller |
| 2 | + |
| 3 | +Simple implementation for `Nette\Application\IPresenter`. |
| 4 | + |
| 5 | +This allows to completely avoid `Nette\Application\UI\Presenter` and focus only on stateless API actions; ApiController avoids e.g.: |
| 6 | +- Nette Component model (components hierarchy, forms, custom controls, etc.), |
| 7 | +- Signals (handleAction), |
| 8 | +- Creating links, canonicalization requests, |
| 9 | +- Redirects, |
| 10 | +- Templates rendering (you can manually render Latte with Latte Engine but it's not integrated directly inside the Controller) |
| 11 | + |
| 12 | +## Installation |
| 13 | + |
| 14 | + |
| 15 | +- Add dependency ``composer require vitekdev/nette-api-controller`` |
| 16 | +- ApiController uses PSR 3 logging, if you want to use Tracy, just register this bridge in your services config: ``Tracy\Bridges\Psr\TracyToPsrLoggerAdapter`` |
| 17 | +- That's it, you can create your first Controller |
| 18 | + |
| 19 | +## Example |
| 20 | + |
| 21 | +```php |
| 22 | +<?php |
| 23 | + |
| 24 | +declare(strict_types=1); |
| 25 | + |
| 26 | +final readonly class HelloController extends \VitekDev\Nette\Application\ApiController |
| 27 | +{ |
| 28 | + public function __construct( |
| 29 | + private UsersService $usersService, |
| 30 | + ) { |
| 31 | + } |
| 32 | + |
| 33 | + // Controller actions are in httpmethodActionName format |
| 34 | + |
| 35 | + /** |
| 36 | + * @return UserDto[] |
| 37 | + */ |
| 38 | + public function getIndex(): array // List of users |
| 39 | + { |
| 40 | + return $this->usersService->listUsers(); // Automatically translates to JSON |
| 41 | + } |
| 42 | + |
| 43 | + public function postIndex(CreateUser $dto): UserDto // Create new user; automatically maps payload to object |
| 44 | + { |
| 45 | + return $this->usersService->createUser($dto); |
| 46 | + } |
| 47 | + |
| 48 | + public function postSayHello(string $id): string // Say hello to user; automatically maps route parameter to action parameter |
| 49 | + { |
| 50 | + $user = $this->usersService->get($id); |
| 51 | + if (!$user) { |
| 52 | + throw new ResourceNotFound('User not found'); // Automatically returns HTTP 404 Not Found |
| 53 | + } |
| 54 | + |
| 55 | + return sprintf('Hello %s', $user->name); // Automatically sends text response 'Hello John' |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +## Features |
| 61 | + |
| 62 | +### Action name with request method |
| 63 | + |
| 64 | +Expected request method is defined directly in action name. |
| 65 | + |
| 66 | +E.g. `public function getFoo(): void` will accept only GET requests, `public function postFoo(): void` will accept only POST requests. |
| 67 | + |
| 68 | +### Auto mapping of route parameters |
| 69 | + |
| 70 | +Route parameters are automatically mapped to action parameters. |
| 71 | + |
| 72 | +E.g. `public function getFoo(int $id): void` will automatically map route parameter `id` to action parameter `$id`. |
| 73 | +Parameters itself are handled via `Nette\Routing` (see https://doc.nette.org/en/application/routing#toc-mask-and-parameters). |
| 74 | + |
| 75 | +### Auto mapping of request body JSON to object |
| 76 | + |
| 77 | +All objects extending `VitekDev\Nette\Application\Request\RequestBody` are automatically mapped from request body JSON. |
| 78 | + |
| 79 | +Mapping is done via `::map(array $json): self` method. |
| 80 | +Some exceptions thrown here (`ValidationFailed | InvalidArgumentException | DomainException`) are automatically translated to HTTP 400 Bad Request. |
| 81 | + |
| 82 | +E.g. if we have following DTO class: |
| 83 | + |
| 84 | +```php |
| 85 | +class SayHelloDto implements \VitekDev\Nette\Application\Request\RequestBody |
| 86 | +{ |
| 87 | + public string $name; |
| 88 | + |
| 89 | + public static function map(array $json): self |
| 90 | + { |
| 91 | + $dto = new self(); |
| 92 | + $dto->name = $json['name'] ?? throw new InvalidArgumentException('Missing name'); |
| 93 | + return $dto; |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +We can set up following controller action just as simple as: |
| 99 | + |
| 100 | +```php |
| 101 | +public function postHello(SayHelloDto $request): void |
| 102 | +{ |
| 103 | + // do what you have to do |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +#### Validation & fully automated mapping |
| 108 | +You can also use e.g. `Nette\Schema` if you want fully automated validation and/or need more complex validation. |
| 109 | + |
| 110 | +Implementation of `Nette\Schema` and `RequestBody` is already bundled in `VitekDev\Nette\Application\Request\AutoMappedRequestBody`. |
| 111 | + |
| 112 | +If you need to add some additional validation, you can override method `::getCustomRules(): array` in implementing class. |
| 113 | + |
| 114 | +```php |
| 115 | +class SayHelloDto extends \VitekDev\Nette\Application\Request\AutoMappedRequestBody |
| 116 | +{ |
| 117 | + public string $name; |
| 118 | + |
| 119 | + public int $age; |
| 120 | + |
| 121 | + public static function getCustomRules(): array |
| 122 | + { |
| 123 | + return [ |
| 124 | + 'age' => \Nette\Schema\Expect::int()->min(0)->max(90), |
| 125 | + ]; |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +### Action result = Controller response |
| 131 | + |
| 132 | +The ApiController automatically sends response with data returned from action. |
| 133 | + |
| 134 | +```php |
| 135 | +public function getCustomResponse(): \Nette\Application\Response |
| 136 | +{ |
| 137 | + return new Nette\Application\Responses\VoidResponse(); // you can manually send any compatible Response |
| 138 | +} |
| 139 | + |
| 140 | +public function getText(): string |
| 141 | +{ |
| 142 | + return 'hello world'; // will send HTTP 200 response with 'hello world' text |
| 143 | +} |
| 144 | + |
| 145 | +public function getJson(): array |
| 146 | +{ |
| 147 | + return ['foo' => 'bar']; // will send HTTP 200 response with '{"foo":"bar"}' JSON |
| 148 | +} |
| 149 | + |
| 150 | +public function getDto(): SayHelloDto |
| 151 | +{ |
| 152 | + $dto = new SayHelloDto(); |
| 153 | + $dto->name = 'John'; |
| 154 | + return $dto; // will send HTTP 200 response with '{"name":"John"}' JSON |
| 155 | +} |
| 156 | + |
| 157 | +public function getJustNothing(): void |
| 158 | +{ |
| 159 | + // will send HTTP 204 No Content |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +### Automatic handling specific Exceptions |
| 164 | + |
| 165 | +Listed exceptions are automatically translated to `VitekDev\Nette\Application\Response\StatusResponse`. |
| 166 | + |
| 167 | +- `DomainException` => HTTP 500 Internal Server Error (**with exception message**) |
| 168 | +- `VitekDev\Shared\Exceptions\AuthenticationRequired` => HTTP 401 Unauthorized (**with exception message**) |
| 169 | +- `VitekDev\Shared\Exceptions\AuthorizationInsufficient` => HTTP 403 Forbidden (**with exception message**) |
| 170 | +- `VitekDev\Shared\Exceptions\ResourceNotFound` => HTTP 404 Not Found (**with exception message**) |
| 171 | + |
| 172 | +All other errors, especially `RuntimeException`, are translated to HTTP 500 Internal Server Error **<u>without</u> exception message**. |
| 173 | + |
| 174 | +## Recommended Router |
| 175 | + |
| 176 | +You can use Route as simple as: `api/v1/<module>/<presenter>/<action>[/<id>]` that is already bundled and ready to use: `VitekDev\Nette\Application\ApiRouter`. |
| 177 | + |
| 178 | + |
| 179 | +## Side notes |
| 180 | + |
| 181 | +### Stateless |
| 182 | + |
| 183 | +The ApiController is readonly so all child classes must be readonly. |
0 commit comments