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
<?php
declare(strict_types=1);
final readonly class HelloController extends \VitekDev\Nette\Application\ApiController
{
public function __construct(
private UsersService $usersService,
) {
}
// Controller actions are in httpmethodActionName format
/**
* @return UserDto[]
*/
public function getIndex(): array // List of users
{
return $this->usersService->listUsers(); // Automatically translates to JSON
}
public function postIndex(CreateUser $dto): UserDto // Create new user; automatically maps payload to object
{
return $this->usersService->createUser($dto);
}
public function postSayHello(string $id): string // Say hello to user; automatically maps route parameter to action parameter
{
$user = $this->usersService->get($id);
if (!$user) {
throw new ResourceNotFound('User not found'); // Automatically returns HTTP 404 Not Found
}
return sprintf('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
{
public string $name;
public static function map(array $json): self
{
$dto = new self();
$dto->name = $json['name'] ?? throw new InvalidArgumentException('Missing name');
return $dto;
}
}
We can set up following controller action just as simple as:
public function postHello(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
{
public string $name;
public int $age;
public static function getCustomRules(): array
{
return [
'age' => \Nette\Schema\Expect::int()->min(0)->max(90),
];
}
}
The ApiController automatically sends response with data returned from action.
public function getCustomResponse(): \Nette\Application\Response
{
return new Nette\Application\Responses\VoidResponse(); // you can manually send any compatible Response
}
public function getText(): string
{
return 'hello world'; // will send HTTP 200 response with 'hello world' text
}
public function getJson(): array
{
return ['foo' => 'bar']; // will send HTTP 200 response with '{"foo":"bar"}' JSON
}
public function getDto(): SayHelloDto
{
$dto = new SayHelloDto();
$dto->name = 'John';
return $dto; // will send HTTP 200 response with '{"name":"John"}' JSON
}
public function getJustNothing(): 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.