Skip to content

Commit cd30488

Browse files
authored
Version 3.0.0 (#154)
### Changed * Support for semantic versioning api. * [BC] DefaultHandler response code 404 instead 400 * [BC] Added Container to API Decider * [BC] Output Configurator, Allows different methods for output configuration. Needs to be added to config services. * [BC] Error handler, Allows for custom error handling of handle method. Needs to be added to config services. * Query configurator rework ### Added * CorsPreflightHandlerInterface - resolve multiple service registered handler error * Lazy API handlers
1 parent 7b6c796 commit cd30488

29 files changed

+660
-120
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,25 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
44

55
## [Unreleased][unreleased]
66

7+
## 3.0.0
8+
9+
### Changed
10+
* Support for semantic versioning api.
11+
* [BC] DefaultHandler response code 404 instead 400
12+
* [BC] Added Container to API Decider
13+
* [BC] Output Configurator, Allows different methods for output configuration. Needs to be added to config services.
14+
* [BC] Error handler, Allows for custom error handling of handle method. Needs to be added to config services.
15+
* Query configurator rework
16+
17+
### Added
18+
* CorsPreflightHandlerInterface - resolve multiple service registered handler error
19+
* Lazy API handlers
20+
721
## 2.12.0
822

923
### Added
1024
* Button to copy `Body` content in api console
25+
* Ability to disable schema validation and provide additional error info with get parameters.
1126

1227
### Changed
1328
* Handler tag wrapper has changed class from `btn` to `label`

README.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ application:
4040
Api: Tomaj\NetteApi\Presenters\*Presenter
4141
```
4242

43+
Register your preferred output configurator in *config.neon* services:
44+
45+
```neon
46+
services:
47+
apiOutputConfigurator: Tomaj\NetteApi\Output\Configurator\DebuggerConfigurator
48+
```
49+
50+
Register your preferred error handler in *config.neon* services:
51+
52+
```neon
53+
services:
54+
apiErrorHandler: Tomaj\NetteApi\Error\DefaultErrorHandler
55+
```
56+
4357
And add route to you RouterFactory:
4458

4559
```php
@@ -64,13 +78,26 @@ After that you need only register your API handlers to *apiDecider* [ApiDecider]
6478

6579
```neon
6680
services:
67-
- Tomaj\NetteApi\Link\ApiLink
68-
- Tomaj\NetteApi\Misc\IpDetector
69-
apiDecider:
81+
- Tomaj\NetteApi\Link\ApiLink
82+
- Tomaj\NetteApi\Misc\IpDetector
83+
apiDecider:
84+
factory: Tomaj\NetteApi\ApiDecider
85+
setup:
86+
- addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization())
87+
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())
88+
89+
```
90+
91+
or lazy (preferred because of performance)
92+
```neon
93+
services:
94+
- App\MyApi\v1\Handlers\SendEmailLazyHandler()
95+
sendEmailLazyNamed: App\MyApi\v1\Handlers\SendEmailLazyNamedHandler()
96+
7097
factory: Tomaj\NetteApi\ApiDecider
7198
setup:
72-
- addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization())
73-
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())
99+
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy'), 'App\MyApi\v1\Handlers\SendEmailHandler', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())
100+
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy-named'), '@sendEmailLazyNamed', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())
74101
```
75102

76103
As you can see in example, you can register as many endpoints as you want with different configurations. Nette-Api supports API versioning from the beginning.
@@ -329,6 +356,7 @@ services:
329356
- addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\BasicBasicAuthentication(['first-user': 'first-password', 'second-user': 'second-password']))
330357
```
331358

359+
332360
### Bearer token authentication
333361
For simple use of Bearer token authorization with few tokens, you can use [StaticTokenRepository](src/Misc/StaticTokenRepository.php) (Tomaj\NetteApi\Misc\StaticTokenRepository).
334362

src/Api.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@ class Api
1919

2020
private $rateLimit;
2121

22+
/**
23+
* @param EndpointInterface $endpoint
24+
* @param ApiHandlerInterface|string $handler
25+
* @param ApiAuthorizationInterface $authorization
26+
* @param RateLimitInterface|null $rateLimit
27+
*/
2228
public function __construct(
2329
EndpointInterface $endpoint,
24-
ApiHandlerInterface $handler,
30+
$handler,
2531
ApiAuthorizationInterface $authorization,
2632
?RateLimitInterface $rateLimit = null
2733
) {
@@ -36,7 +42,10 @@ public function getEndpoint(): EndpointInterface
3642
return $this->endpoint;
3743
}
3844

39-
public function getHandler(): ApiHandlerInterface
45+
/**
46+
* @return ApiHandlerInterface|string
47+
*/
48+
public function getHandler()
4049
{
4150
return $this->handler;
4251
}

src/ApiDecider.php

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,44 @@
44

55
namespace Tomaj\NetteApi;
66

7+
use Nette\DI\Container;
78
use Nette\Http\Response;
89
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
910
use Tomaj\NetteApi\Authorization\NoAuthorization;
1011
use Tomaj\NetteApi\Handlers\ApiHandlerInterface;
1112
use Tomaj\NetteApi\Handlers\CorsPreflightHandler;
1213
use Tomaj\NetteApi\Handlers\DefaultHandler;
1314
use Tomaj\NetteApi\RateLimit\RateLimitInterface;
15+
use Tomaj\NetteApi\Handlers\CorsPreflightHandlerInterface;
1416

1517
class ApiDecider
1618
{
19+
/** @var Container */
20+
private $container;
21+
1722
/** @var Api[] */
1823
private $apis = [];
1924

2025
/** @var ApiHandlerInterface|null */
2126
private $globalPreflightHandler = null;
2227

28+
public function __construct(Container $container)
29+
{
30+
$this->container = $container;
31+
}
32+
2333
/**
2434
* Get api handler that match input method, version, package and apiAction.
2535
* If decider cannot find handler for given handler, returns defaults.
2636
*
2737
* @param string $method
28-
* @param integer $version
38+
* @param string $version
2939
* @param string $package
3040
* @param string $apiAction
3141
*
3242
* @return Api
3343
*/
34-
public function getApi(string $method, int $version, string $package, ?string $apiAction = null)
44+
public function getApi(string $method, string $version, string $package, ?string $apiAction = null)
3545
{
3646
$method = strtoupper($method);
3747
$apiAction = $apiAction === '' ? null : $apiAction;
@@ -40,8 +50,9 @@ public function getApi(string $method, int $version, string $package, ?string $a
4050
$identifier = $api->getEndpoint();
4151
if ($method === $identifier->getMethod() && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) {
4252
$endpointIdentifier = new EndpointIdentifier($method, $version, $package, $apiAction);
43-
$api->getHandler()->setEndpointIdentifier($endpointIdentifier);
44-
return $api;
53+
$handler = $this->getHandler($api);
54+
$handler->setEndpointIdentifier($endpointIdentifier);
55+
return new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit());
4556
}
4657
if ($method === 'OPTIONS' && $this->globalPreflightHandler && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) {
4758
return new Api(new EndpointIdentifier('OPTIONS', $version, $package, $apiAction), $this->globalPreflightHandler, new NoAuthorization());
@@ -50,7 +61,7 @@ public function getApi(string $method, int $version, string $package, ?string $a
5061
return new Api(new EndpointIdentifier($method, $version, $package, $apiAction), new DefaultHandler(), new NoAuthorization());
5162
}
5263

53-
public function enableGlobalPreflight(ApiHandlerInterface $corsHandler = null)
64+
public function enableGlobalPreflight(CorsPreflightHandlerInterface $corsHandler = null)
5465
{
5566
if (!$corsHandler) {
5667
$corsHandler = new CorsPreflightHandler(new Response());
@@ -62,12 +73,12 @@ public function enableGlobalPreflight(ApiHandlerInterface $corsHandler = null)
6273
* Register new api handler
6374
*
6475
* @param EndpointInterface $endpointIdentifier
65-
* @param ApiHandlerInterface $handler
76+
* @param ApiHandlerInterface|string $handler
6677
* @param ApiAuthorizationInterface $apiAuthorization
6778
* @param RateLimitInterface|null $rateLimit
6879
* @return self
6980
*/
70-
public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterface $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self
81+
public function addApi(EndpointInterface $endpointIdentifier, $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self
7182
{
7283
$this->apis[] = new Api($endpointIdentifier, $handler, $apiAuthorization, $rateLimit);
7384
return $this;
@@ -80,6 +91,25 @@ public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterfac
8091
*/
8192
public function getApis(): array
8293
{
83-
return $this->apis;
94+
$apis = [];
95+
foreach ($this->apis as $api) {
96+
$handler = $this->getHandler($api);
97+
$apis[] = new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit());
98+
}
99+
return $apis;
100+
}
101+
102+
private function getHandler(Api $api): ApiHandlerInterface
103+
{
104+
$handler = $api->getHandler();
105+
if (!is_string($handler)) {
106+
return $handler;
107+
}
108+
109+
if (str_starts_with($handler, '@')) {
110+
return $this->container->getByName(substr($handler, 1));
111+
}
112+
113+
return $this->container->getByType($handler);
84114
}
85115
}

src/Component/ApiListingControl.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function render(): void
3737
$template->render();
3838
}
3939

40-
public function handleSelect(string $method, int $version, string $package, ?string $apiAction = null): void
40+
public function handleSelect(string $method, $version, string $package, ?string $apiAction = null): void
4141
{
4242
$this->onClick($method, $version, $package, $apiAction);
4343
}

src/EndpointIdentifier.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Tomaj\NetteApi;
66

7+
use InvalidArgumentException;
8+
79
class EndpointIdentifier implements EndpointInterface
810
{
911
private $method;
@@ -14,9 +16,19 @@ class EndpointIdentifier implements EndpointInterface
1416

1517
private $apiAction;
1618

17-
public function __construct(string $method, int $version, string $package, ?string $apiAction = null)
19+
/**
20+
* @param string $method example: "GET", "POST", "PUT", "DELETE"
21+
* @param string|int $version Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc.
22+
* @param string $package example: "users"
23+
* @param string|null $apiAction example: "query"
24+
*/
25+
public function __construct(string $method, $version, string $package, ?string $apiAction = null)
1826
{
27+
$version = (string) $version;
1928
$this->method = strtoupper($method);
29+
if (strpos($version, '/') !== false) {
30+
throw new InvalidArgumentException('Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc.');
31+
}
2032
$this->version = $version;
2133
$this->package = $package;
2234
$this->apiAction = $apiAction;
@@ -27,7 +39,7 @@ public function getMethod(): string
2739
return $this->method;
2840
}
2941

30-
public function getVersion(): int
42+
public function getVersion(): string
3143
{
3244
return $this->version;
3345
}

src/EndpointInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface EndpointInterface
88
{
99
public function getMethod(): string;
1010

11-
public function getVersion(): int;
11+
public function getVersion(): string;
1212

1313
public function getPackage(): string;
1414

src/Error/DefaultErrorHandler.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tomaj\NetteApi\Error;
6+
7+
use Nette\Http\Response;
8+
use Throwable;
9+
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
10+
use Tomaj\NetteApi\Output\Configurator\ConfiguratorInterface;
11+
use Tomaj\NetteApi\Response\JsonApiResponse;
12+
use Tracy\Debugger;
13+
14+
final class DefaultErrorHandler implements ErrorHandlerInterface
15+
{
16+
/** @var ConfiguratorInterface */
17+
private $outputConfigurator;
18+
19+
public function __construct(ConfiguratorInterface $outputConfigurator)
20+
{
21+
$this->outputConfigurator = $outputConfigurator;
22+
}
23+
24+
public function handle(Throwable $exception, array $params): JsonApiResponse
25+
{
26+
Debugger::log($exception, Debugger::EXCEPTION);
27+
if ($this->outputConfigurator->showErrorDetail()) {
28+
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]);
29+
} else {
30+
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']);
31+
}
32+
return $response;
33+
}
34+
35+
public function handleInputParams(array $errors): JsonApiResponse
36+
{
37+
if ($this->outputConfigurator->showErrorDetail()) {
38+
$response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input', 'detail' => $errors]);
39+
} else {
40+
$response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input']);
41+
}
42+
return $response;
43+
}
44+
45+
public function handleSchema(array $errors, array $params): JsonApiResponse
46+
{
47+
Debugger::log($errors, Debugger::ERROR);
48+
49+
if ($this->outputConfigurator->showErrorDetail()) {
50+
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $errors]);
51+
} else {
52+
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']);
53+
}
54+
return $response;
55+
}
56+
57+
public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse
58+
{
59+
return new JsonApiResponse(Response::S401_UNAUTHORIZED, ['status' => 'error', 'message' => $auth->getErrorMessage()]);
60+
}
61+
62+
public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse
63+
{
64+
return new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => $exception->getMessage()]);
65+
}
66+
}

src/Error/ErrorHandlerInterface.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tomaj\NetteApi\Error;
6+
7+
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
8+
use Tomaj\NetteApi\Response\JsonApiResponse;
9+
use Throwable;
10+
11+
interface ErrorHandlerInterface
12+
{
13+
/**
14+
* @param array<mixed> $params
15+
*/
16+
public function handle(Throwable $exception, array $params): JsonApiResponse;
17+
18+
/**
19+
* @param array<string> $errors
20+
* @param array<mixed> $params
21+
*/
22+
public function handleInputParams(array $errors): JsonApiResponse;
23+
24+
/**
25+
* @param array<string> $errors
26+
* @param array<mixed> $params
27+
*/
28+
public function handleSchema(array $errors, array $params): JsonApiResponse;
29+
30+
/**
31+
* @param array<mixed> $params
32+
*/
33+
public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse;
34+
35+
/**
36+
* @param array<mixed> $params
37+
*/
38+
public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse;
39+
}

src/Handlers/ApiListingHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function handle(array $params): ResponseInterface
5353
*
5454
* @return array
5555
*/
56-
private function getApiList(int $version): array
56+
private function getApiList(string $version): array
5757
{
5858
$versionApis = array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) {
5959
return $version === $api->getEndpoint()->getVersion();

0 commit comments

Comments
 (0)