Skip to content

Commit 4ea2491

Browse files
author
Daniel Vitek
committed
Initial version of Api Controller
1 parent 6908cd9 commit 4ea2491

18 files changed

+3447
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
vendor
3+
4+
.phpunit.result.cache

README.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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.

composer.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "vitek-dev/nette-api-controller",
3+
"description": "Simple API handling implementation for Nette\\Application\\IPresenter",
4+
"type": "library",
5+
"license": "MIT",
6+
"autoload": {
7+
"psr-4": {
8+
"VitekDev\\Nette\\": "src/"
9+
}
10+
},
11+
"autoload-dev": {
12+
"psr-4": {
13+
"VitekDev\\Tests\\Nette\\": "tests/"
14+
}
15+
},
16+
"authors": [
17+
{
18+
"name": "Daniel Vitek",
19+
"email": "daniel@vitek.dev"
20+
}
21+
],
22+
"require": {
23+
"php": ">=8.3",
24+
"ext-mbstring": "*",
25+
"vitek-dev/exceptions": "^1.0",
26+
"nette/application": "^3.2",
27+
"nette/schema": "^1.3",
28+
"psr/log": "^3.0"
29+
},
30+
"require-dev": {
31+
"phpunit/phpunit": "^10.0"
32+
}
33+
}

0 commit comments

Comments
 (0)