Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip

## [Unreleased][unreleased]

## 3.0.0

### 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

## 2.12.0

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

### Changed
* Handler tag wrapper has changed class from `btn` to `label`
Expand Down
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ application:
Api: Tomaj\NetteApi\Presenters\*Presenter
```

Register your preferred output configurator in *config.neon* services:

```neon
services:
apiOutputConfigurator: Tomaj\NetteApi\Output\Configurator\DebuggerConfigurator
```

Register your preferred error handler in *config.neon* services:

```neon
services:
apiErrorHandler: Tomaj\NetteApi\Error\DefaultErrorHandler
```

And add route to you RouterFactory:

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

```neon
services:
- Tomaj\NetteApi\Link\ApiLink
- Tomaj\NetteApi\Misc\IpDetector
apiDecider:
- Tomaj\NetteApi\Link\ApiLink
- Tomaj\NetteApi\Misc\IpDetector
apiDecider:
factory: Tomaj\NetteApi\ApiDecider
setup:
- addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization())
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())

```

or lazy (preferred because of performance)
```neon
services:
- App\MyApi\v1\Handlers\SendEmailLazyHandler()
sendEmailLazyNamed: App\MyApi\v1\Handlers\SendEmailLazyNamedHandler()

factory: Tomaj\NetteApi\ApiDecider
setup:
- addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization())
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy'), 'App\MyApi\v1\Handlers\SendEmailHandler', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())
- addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy-named'), '@sendEmailLazyNamed', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())
```

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.
Expand Down Expand Up @@ -329,6 +356,7 @@ services:
- addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\BasicBasicAuthentication(['first-user': 'first-password', 'second-user': 'second-password']))
```


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

Expand Down
13 changes: 11 additions & 2 deletions src/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ class Api

private $rateLimit;

/**
* @param EndpointInterface $endpoint
* @param ApiHandlerInterface|string $handler
* @param ApiAuthorizationInterface $authorization
* @param RateLimitInterface|null $rateLimit
*/
public function __construct(
EndpointInterface $endpoint,
ApiHandlerInterface $handler,
$handler,
ApiAuthorizationInterface $authorization,
?RateLimitInterface $rateLimit = null
) {
Expand All @@ -36,7 +42,10 @@ public function getEndpoint(): EndpointInterface
return $this->endpoint;
}

public function getHandler(): ApiHandlerInterface
/**
* @return ApiHandlerInterface|string
*/
public function getHandler()
{
return $this->handler;
}
Expand Down
46 changes: 38 additions & 8 deletions src/ApiDecider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,44 @@

namespace Tomaj\NetteApi;

use Nette\DI\Container;
use Nette\Http\Response;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
use Tomaj\NetteApi\Authorization\NoAuthorization;
use Tomaj\NetteApi\Handlers\ApiHandlerInterface;
use Tomaj\NetteApi\Handlers\CorsPreflightHandler;
use Tomaj\NetteApi\Handlers\DefaultHandler;
use Tomaj\NetteApi\RateLimit\RateLimitInterface;
use Tomaj\NetteApi\Handlers\CorsPreflightHandlerInterface;

class ApiDecider
{
/** @var Container */
private $container;

/** @var Api[] */
private $apis = [];

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

public function __construct(Container $container)
{
$this->container = $container;
}

/**
* Get api handler that match input method, version, package and apiAction.
* If decider cannot find handler for given handler, returns defaults.
*
* @param string $method
* @param integer $version
* @param string $version
* @param string $package
* @param string $apiAction
*
* @return Api
*/
public function getApi(string $method, int $version, string $package, ?string $apiAction = null)
public function getApi(string $method, string $version, string $package, ?string $apiAction = null)
{
$method = strtoupper($method);
$apiAction = $apiAction === '' ? null : $apiAction;
Expand All @@ -40,8 +50,9 @@ public function getApi(string $method, int $version, string $package, ?string $a
$identifier = $api->getEndpoint();
if ($method === $identifier->getMethod() && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) {
$endpointIdentifier = new EndpointIdentifier($method, $version, $package, $apiAction);
$api->getHandler()->setEndpointIdentifier($endpointIdentifier);
return $api;
$handler = $this->getHandler($api);
$handler->setEndpointIdentifier($endpointIdentifier);
return new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit());
}
if ($method === 'OPTIONS' && $this->globalPreflightHandler && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) {
return new Api(new EndpointIdentifier('OPTIONS', $version, $package, $apiAction), $this->globalPreflightHandler, new NoAuthorization());
Expand All @@ -50,7 +61,7 @@ public function getApi(string $method, int $version, string $package, ?string $a
return new Api(new EndpointIdentifier($method, $version, $package, $apiAction), new DefaultHandler(), new NoAuthorization());
}

public function enableGlobalPreflight(ApiHandlerInterface $corsHandler = null)
public function enableGlobalPreflight(CorsPreflightHandlerInterface $corsHandler = null)
{
if (!$corsHandler) {
$corsHandler = new CorsPreflightHandler(new Response());
Expand All @@ -62,12 +73,12 @@ public function enableGlobalPreflight(ApiHandlerInterface $corsHandler = null)
* Register new api handler
*
* @param EndpointInterface $endpointIdentifier
* @param ApiHandlerInterface $handler
* @param ApiHandlerInterface|string $handler
* @param ApiAuthorizationInterface $apiAuthorization
* @param RateLimitInterface|null $rateLimit
* @return self
*/
public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterface $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self
public function addApi(EndpointInterface $endpointIdentifier, $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self
{
$this->apis[] = new Api($endpointIdentifier, $handler, $apiAuthorization, $rateLimit);
return $this;
Expand All @@ -80,6 +91,25 @@ public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterfac
*/
public function getApis(): array
{
return $this->apis;
$apis = [];
foreach ($this->apis as $api) {
$handler = $this->getHandler($api);
$apis[] = new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit());
}
return $apis;
}

private function getHandler(Api $api): ApiHandlerInterface
{
$handler = $api->getHandler();
if (!is_string($handler)) {
return $handler;
}

if (str_starts_with($handler, '@')) {
return $this->container->getByName(substr($handler, 1));
}

return $this->container->getByType($handler);
}
}
2 changes: 1 addition & 1 deletion src/Component/ApiListingControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function render(): void
$template->render();
}

public function handleSelect(string $method, int $version, string $package, ?string $apiAction = null): void
public function handleSelect(string $method, $version, string $package, ?string $apiAction = null): void
{
$this->onClick($method, $version, $package, $apiAction);
}
Expand Down
16 changes: 14 additions & 2 deletions src/EndpointIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Tomaj\NetteApi;

use InvalidArgumentException;

class EndpointIdentifier implements EndpointInterface
{
private $method;
Expand All @@ -14,9 +16,19 @@ class EndpointIdentifier implements EndpointInterface

private $apiAction;

public function __construct(string $method, int $version, string $package, ?string $apiAction = null)
/**
* @param string $method example: "GET", "POST", "PUT", "DELETE"
* @param string|int $version Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc.
* @param string $package example: "users"
* @param string|null $apiAction example: "query"
*/
public function __construct(string $method, $version, string $package, ?string $apiAction = null)
{
$version = (string) $version;
$this->method = strtoupper($method);
if (strpos($version, '/') !== false) {
throw new InvalidArgumentException('Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc.');
}
$this->version = $version;
$this->package = $package;
$this->apiAction = $apiAction;
Expand All @@ -27,7 +39,7 @@ public function getMethod(): string
return $this->method;
}

public function getVersion(): int
public function getVersion(): string
{
return $this->version;
}
Expand Down
2 changes: 1 addition & 1 deletion src/EndpointInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface EndpointInterface
{
public function getMethod(): string;

public function getVersion(): int;
public function getVersion(): string;

public function getPackage(): string;

Expand Down
66 changes: 66 additions & 0 deletions src/Error/DefaultErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Tomaj\NetteApi\Error;

use Nette\Http\Response;
use Throwable;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
use Tomaj\NetteApi\Output\Configurator\ConfiguratorInterface;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tracy\Debugger;

final class DefaultErrorHandler implements ErrorHandlerInterface
{
/** @var ConfiguratorInterface */
private $outputConfigurator;

public function __construct(ConfiguratorInterface $outputConfigurator)
{
$this->outputConfigurator = $outputConfigurator;
}

public function handle(Throwable $exception, array $params): JsonApiResponse
{
Debugger::log($exception, Debugger::EXCEPTION);
if ($this->outputConfigurator->showErrorDetail()) {
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]);
} else {
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']);
}
return $response;
}

public function handleInputParams(array $errors): JsonApiResponse
{
if ($this->outputConfigurator->showErrorDetail()) {
$response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input', 'detail' => $errors]);
} else {
$response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input']);
}
return $response;
}

public function handleSchema(array $errors, array $params): JsonApiResponse
{
Debugger::log($errors, Debugger::ERROR);

if ($this->outputConfigurator->showErrorDetail()) {
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $errors]);
} else {
$response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']);
}
return $response;
}

public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse
{
return new JsonApiResponse(Response::S401_UNAUTHORIZED, ['status' => 'error', 'message' => $auth->getErrorMessage()]);
}

public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse
{
return new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => $exception->getMessage()]);
}
}
39 changes: 39 additions & 0 deletions src/Error/ErrorHandlerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Tomaj\NetteApi\Error;

use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Throwable;

interface ErrorHandlerInterface
{
/**
* @param array<mixed> $params
*/
public function handle(Throwable $exception, array $params): JsonApiResponse;

/**
* @param array<string> $errors
* @param array<mixed> $params
*/
public function handleInputParams(array $errors): JsonApiResponse;

/**
* @param array<string> $errors
* @param array<mixed> $params
*/
public function handleSchema(array $errors, array $params): JsonApiResponse;

/**
* @param array<mixed> $params
*/
public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse;

/**
* @param array<mixed> $params
*/
public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse;
}
2 changes: 1 addition & 1 deletion src/Handlers/ApiListingHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function handle(array $params): ResponseInterface
*
* @return array
*/
private function getApiList(int $version): array
private function getApiList(string $version): array
{
$versionApis = array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) {
return $version === $api->getEndpoint()->getVersion();
Expand Down
Loading