Simple implementation for Nette\Application\IPresenter.
This allows to completely avoid Nette\Application\UI\Presenter and focus only on stateless API actions; ApiController avoids e.g.:
- Nette Component model (components hierarchy, forms, custom controls, etc.),
- Signals (handleAction),
- Creating links, canonicalization requests,
- Redirects,
- Templates rendering (you can manually render Latte with Latte Engine but it's not integrated directly inside the Controller)
- Add dependency
composer require vitekdev/nette-api-controller - ApiController uses PSR 3 logging, if you want to use Tracy, just register this bridge in your services config:
Tracy\Bridges\Psr\TracyToPsrLoggerAdapter - That's it, you can create your first Controller
<?phpdeclare(strict_types=1);
finalreadonlyclass HelloController extends \VitekDev\Nette\Application\ApiController
{
publicfunction__construct(
privateUsersService$usersService,
) {
}
// Controller actions are in httpmethodActionName format/** * @return UserDto[] */publicfunctiongetIndex(): array// List of users
{
return$this->usersService->listUsers(); // Automatically translates to JSON
}
publicfunctionpostIndex(CreateUser$dto): UserDto// Create new user; automatically maps payload to object
{
return$this->usersService->createUser($dto);
}
publicfunctionpostSayHello(string$id): string// Say hello to user; automatically maps route parameter to action parameter
{
$user = $this->usersService->get($id);
if (!$user) {
thrownewResourceNotFound('User not found'); // Automatically returns HTTP 404 Not Found
}
returnsprintf('Hello %s', $user->name); // Automatically sends text response 'Hello John'
}
}Expected request method is defined directly in action name.
E.g. public function getFoo(): void will accept only GET requests, public function postFoo(): void will accept only POST requests.
Route parameters are automatically mapped to action parameters.
E.g. public function getFoo(int $id): void will automatically map route parameter id to action parameter $id.
Parameters itself are handled via Nette\Routing (see https://doc.nette.org/en/application/routing#toc-mask-and-parameters).
All objects extending VitekDev\Nette\Application\Request\RequestBody are automatically mapped from request body JSON.
Mapping is done via ::map(array $json): self method.
Some exceptions thrown here (ValidationFailed | InvalidArgumentException | DomainException) are automatically translated to HTTP 400 Bad Request.
E.g. if we have following DTO class:
class SayHelloDto implements \VitekDev\Nette\Application\Request\RequestBody
{
publicstring$name;
publicstaticfunctionmap(array$json): self
{
$dto = newself();
$dto->name = $json['name'] ?? thrownewInvalidArgumentException('Missing name');
return$dto;
}
}We can set up following controller action just as simple as:
publicfunctionpostHello(SayHelloDto$request): void
{
// do what you have to do
}You can also use e.g. Nette\Schema if you want fully automated validation and/or need more complex validation.
Implementation of Nette\Schema and RequestBody is already bundled in VitekDev\Nette\Application\Request\AutoMappedRequestBody.
If you need to add some additional validation, you can override method ::getCustomRules(): array in implementing class.
class SayHelloDto extends \VitekDev\Nette\Application\Request\AutoMappedRequestBody
{
publicstring$name;
publicint$age;
publicstaticfunctiongetCustomRules(): array
{
return [
'age' => \Nette\Schema\Expect::int()->min(0)->max(90),
];
}
}The ApiController automatically sends response with data returned from action.
publicfunctiongetCustomResponse(): \Nette\Application\Response
{
returnnewNette\Application\Responses\VoidResponse(); // you can manually send any compatible Response
}
publicfunctiongetText(): string
{
return'hello world'; // will send HTTP 200 response with 'hello world' text
}
publicfunctiongetJson(): array
{
return ['foo' => 'bar']; // will send HTTP 200 response with '{"foo":"bar"}' JSON
}
publicfunctiongetDto(): SayHelloDto
{
$dto = newSayHelloDto();
$dto->name = 'John';
return$dto; // will send HTTP 200 response with '{"name":"John"}' JSON
}
publicfunctiongetJustNothing(): void
{
// will send HTTP 204 No Content
}Listed exceptions are automatically translated to VitekDev\Nette\Application\Response\StatusResponse.
DomainException=> HTTP 500 Internal Server Error (with exception message)VitekDev\Shared\Exceptions\AuthenticationRequired=> HTTP 401 Unauthorized (with exception message)VitekDev\Shared\Exceptions\AuthorizationInsufficient=> HTTP 403 Forbidden (with exception message)VitekDev\Shared\Exceptions\ResourceNotFound=> HTTP 404 Not Found (with exception message)
All other errors, especially RuntimeException, are translated to HTTP 500 Internal Server Error without exception message.
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.
The ApiController is readonly so all child classes must be readonly.