diff --git a/.gitignore b/.gitignore index ad3521a..829515f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vendor build coverage.clover .idea +.phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfbecd..c3a3a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Changed * Update Request body schema in OpenApiHandler * Set null value when process empty file params, instead of false +* [BC] Update to php >= 8.3, support for 8.4 +* [BC] addApi register method now requires ApiHandlerInterface as second parameter instead of string or service name ## 3.0.0 diff --git a/composer.json b/composer.json index 9c52e1f..d626d49 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": ">= 7.1.0", + "php": ">= 8.3", "ext-curl": "*", "ext-json": "*", "ext-session": "*", @@ -21,11 +21,11 @@ "tracy/tracy": "^2.6", "league/fractal": "~0.17", "tomaj/nette-bootstrap-form": "^2.0", - "justinrainbow/json-schema": "^5.2" + "justinrainbow/json-schema": "^5.2", + "latte/latte": "^3.0" }, "require-dev": { "nette/di": "^3.0", - "latte/latte": "^2.4 | ^3.0", "phpunit/phpunit": ">7.0 <10.0", "symfony/yaml": "^4.4|^5.0|^6.0", "squizlabs/php_codesniffer": "^3.2" diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..42802a8 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,12 @@ +parameters: + level: 8 + paths: + - src + excludePaths: + - vendor + - tests + ignoreErrors: + # Ignore magic number warnings in Nette SmartObject trait (vendor code) + - + message: '#^Do not use magic number in bitwise operations\. Move to constant with a suitable name\.$#' + path: 'vendor/nette/utils/src/SmartObject.php' diff --git a/src/Api.php b/src/Api.php index 5c53491..a47da1d 100644 --- a/src/Api.php +++ b/src/Api.php @@ -20,10 +20,7 @@ class Api private $rateLimit; /** - * @param EndpointInterface $endpoint - * @param ApiHandlerInterface|string $handler - * @param ApiAuthorizationInterface $authorization - * @param RateLimitInterface|null $rateLimit + * @param ApiHandlerInterface $handler */ public function __construct( EndpointInterface $endpoint, @@ -42,10 +39,7 @@ public function getEndpoint(): EndpointInterface return $this->endpoint; } - /** - * @return ApiHandlerInterface|string - */ - public function getHandler() + public function getHandler(): ApiHandlerInterface { return $this->handler; } diff --git a/src/ApiDecider.php b/src/ApiDecider.php index 541bd2e..4220bc6 100644 --- a/src/ApiDecider.php +++ b/src/ApiDecider.php @@ -4,39 +4,26 @@ 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\CorsPreflightHandlerInterface; 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; - } + private ?ApiHandlerInterface $globalPreflightHandler = null; /** * 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 string $version - * @param string $package * @param string $apiAction * * @return Api @@ -54,31 +41,28 @@ public function getApi(string $method, string $version, string $package, ?string $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()); } } + return new Api(new EndpointIdentifier($method, $version, $package, $apiAction), new DefaultHandler(), new NoAuthorization()); } - public function enableGlobalPreflight(CorsPreflightHandlerInterface $corsHandler = null) + public function enableGlobalPreflight(?CorsPreflightHandlerInterface $corsHandler = null): void { if (!$corsHandler) { $corsHandler = new CorsPreflightHandler(new Response()); } + $this->globalPreflightHandler = $corsHandler; } /** * Register new api handler - * - * @param EndpointInterface $endpointIdentifier - * @param ApiHandlerInterface|string $handler - * @param ApiAuthorizationInterface $apiAuthorization - * @param RateLimitInterface|null $rateLimit - * @return self */ - public function addApi(EndpointInterface $endpointIdentifier, $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self + public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterface $handler, ApiAuthorizationInterface $apiAuthorization, ?RateLimitInterface $rateLimit = null): self { $this->apis[] = new Api($endpointIdentifier, $handler, $apiAuthorization, $rateLimit); return $this; @@ -96,20 +80,12 @@ public function getApis(): array $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); + return $api->getHandler(); } } diff --git a/src/Authorization/ApiAuthorizationInterface.php b/src/Authorization/ApiAuthorizationInterface.php index db37947..8dc12cf 100644 --- a/src/Authorization/ApiAuthorizationInterface.php +++ b/src/Authorization/ApiAuthorizationInterface.php @@ -8,16 +8,12 @@ interface ApiAuthorizationInterface { /** * Main method to check if this authorization authorize actual request. - * - * @return boolean */ public function authorized(): bool; /** * If authorization deny acces, this method should provide additional information * abount cause of restriction. - * - * @return string|null */ public function getErrorMessage(): ?string; } diff --git a/src/Authorization/BasicAuthentication.php b/src/Authorization/BasicAuthentication.php index e34a225..7638449 100644 --- a/src/Authorization/BasicAuthentication.php +++ b/src/Authorization/BasicAuthentication.php @@ -8,15 +8,14 @@ class BasicAuthentication implements ApiAuthorizationInterface { - /** @var array */ - private $authentications; + /** @var array */ + private array $authentications; /** @var IRequest */ private $httpRequest; /** * @param array $autentications - available username - password pairs - * @param IRequest $httpRequest */ public function __construct(array $autentications, IRequest $httpRequest) { @@ -34,6 +33,7 @@ public function authorized(): bool if (!$authentication) { return false; } + return $authentication === $urlScript->getPassword(); } diff --git a/src/Authorization/BearerTokenAuthorization.php b/src/Authorization/BearerTokenAuthorization.php index a5cd14b..7604877 100644 --- a/src/Authorization/BearerTokenAuthorization.php +++ b/src/Authorization/BearerTokenAuthorization.php @@ -9,11 +9,10 @@ class BearerTokenAuthorization extends TokenAuthorization { - /** + private const EXPECTED_HTTP_PARTS = 2; + + /** * BearerTokenAuthorization constructor. - * - * @param TokenRepositoryInterface $tokenRepository - * @param IpDetectorInterface $ipDetector */ public function __construct(TokenRepositoryInterface $tokenRepository, IpDetectorInterface $ipDetector) { @@ -23,8 +22,6 @@ public function __construct(TokenRepositoryInterface $tokenRepository, IpDetecto /** * Read HTTP reader with authorization token * If everything is ok, it return token. In other situations returns false and set errorMessage. - * - * @return string|null */ protected function readAuthorizationToken(): ?string { @@ -32,15 +29,18 @@ protected function readAuthorizationToken(): ?string $this->errorMessage = 'Authorization header HTTP_Authorization is not set'; return null; } + $parts = explode(' ', $_SERVER['HTTP_AUTHORIZATION']); - if (count($parts) !== 2) { + if (count($parts) !== self::EXPECTED_HTTP_PARTS) { $this->errorMessage = 'Authorization header contains invalid structure'; return null; } + if (strtolower($parts[0]) !== 'bearer') { $this->errorMessage = 'Authorization header doesn\'t contain bearer token'; return null; } + return $parts[1]; } } diff --git a/src/Authorization/CookieApiKeyAuthentication.php b/src/Authorization/CookieApiKeyAuthentication.php index 4b95051..69bd1bb 100644 --- a/src/Authorization/CookieApiKeyAuthentication.php +++ b/src/Authorization/CookieApiKeyAuthentication.php @@ -24,6 +24,7 @@ protected function readAuthorizationToken(): ?string $this->errorMessage = 'API key is not set'; return null; } + return $apiKey; } diff --git a/src/Authorization/HeaderApiKeyAuthentication.php b/src/Authorization/HeaderApiKeyAuthentication.php index 0843ddc..4693944 100644 --- a/src/Authorization/HeaderApiKeyAuthentication.php +++ b/src/Authorization/HeaderApiKeyAuthentication.php @@ -25,6 +25,7 @@ protected function readAuthorizationToken(): ?string $this->errorMessage = 'API key is not set'; return null; } + return $apiKey; } diff --git a/src/Authorization/QueryApiKeyAuthentication.php b/src/Authorization/QueryApiKeyAuthentication.php index 43a9ff4..22c6b52 100644 --- a/src/Authorization/QueryApiKeyAuthentication.php +++ b/src/Authorization/QueryApiKeyAuthentication.php @@ -24,6 +24,7 @@ protected function readAuthorizationToken(): ?string $this->errorMessage = 'API key is not set'; return null; } + return $apiKey; } diff --git a/src/Authorization/TokenAuthorization.php b/src/Authorization/TokenAuthorization.php index 6378f7d..f5c6c37 100644 --- a/src/Authorization/TokenAuthorization.php +++ b/src/Authorization/TokenAuthorization.php @@ -24,10 +24,6 @@ abstract class TokenAuthorization implements ApiAuthorizationInterface */ protected $ipDetector; - /** - * @param TokenRepositoryInterface $tokenRepository - * @param IpDetectorInterface $ipDetector - */ public function __construct(TokenRepositoryInterface $tokenRepository, IpDetectorInterface $ipDetector) { $this->tokenRepository = $tokenRepository; @@ -74,17 +70,17 @@ public function getErrorMessage(): ?string * '127.0.0.1,127.0.02' - accessible from multiple IP, separator could be new line or space * '127.0.0.1/32' - accessible from ip range * null - disabled access - * - * @return boolean */ private function isValidIp(?string $ipRestrictions): bool { if ($ipRestrictions === null) { return false; } + if ($ipRestrictions === '*' || $ipRestrictions === '') { return true; } + $ip = $this->ipDetector->getRequestIp(); $ipWhiteList = str_replace([',', ' ', "\n"], '#', $ipRestrictions); @@ -93,6 +89,7 @@ private function isValidIp(?string $ipRestrictions): bool if ($whiteIp === $ip) { return true; } + if (strpos($whiteIp, '/') !== false) { return $this->ipInRange($ip, $whiteIp); } @@ -106,14 +103,14 @@ private function isValidIp(?string $ipRestrictions): bool * * @param string $ip this ip will be verified * @param string $range is in IP/CIDR format eg 127.0.0.1/24 - * @return boolean */ private function ipInRange(string $ip, string $range): bool { - list($range, $netmask) = explode('/', $range, 2); + [$range, $netmask] = explode('/', $range, 2); $range_decimal = ip2long($range); $ipDecimal = ip2long($ip); - $wildcard_decimal = pow(2, (32 - (int)$netmask)) - 1; + /** @phpstan-ignore-next-line */ + $wildcard_decimal = 2 ** (32 - (int)$netmask) - 1; $netmask_decimal = ~ $wildcard_decimal; return (($ipDecimal & $netmask_decimal) === ($range_decimal & $netmask_decimal)); } diff --git a/src/Component/ApiConsoleControl.php b/src/Component/ApiConsoleControl.php index c35d67d..7a5b7be 100644 --- a/src/Component/ApiConsoleControl.php +++ b/src/Component/ApiConsoleControl.php @@ -6,8 +6,8 @@ use Nette\Application\UI\Control; use Nette\Application\UI\Form; -use Nette\Bridges\ApplicationLatte\Template; -use Nette\Forms\IFormRenderer; +use Nette\Bridges\ApplicationLatte\DefaultTemplate; +use Nette\Forms\FormRenderer; use Nette\Http\IRequest; use Nette\Utils\ArrayHash; use Tomaj\Form\Renderer\BootstrapVerticalRenderer; @@ -19,40 +19,29 @@ use Tomaj\NetteApi\Handlers\ApiHandlerInterface; use Tomaj\NetteApi\Link\ApiLink; use Tomaj\NetteApi\Misc\ConsoleRequest; -use Tomaj\NetteApi\Component\ApiConsoleFormFactoryInterface; -use Tomaj\NetteApi\Component\DefaultApiConsoleFormFactory; class ApiConsoleControl extends Control { - private $request; + private ApiConsoleFormFactoryInterface $formFactory; - private $endpoint; + private ?FormRenderer $formRenderer = null; - private $handler; + private ?string $templateFilePath = null; - private $authorization; - - private $apiLink; - - private $formFactory; - - private $formRenderer; - - private $templateFilePath; - - public function __construct(IRequest $request, EndpointInterface $endpoint, ApiHandlerInterface $handler, ApiAuthorizationInterface $authorization, ApiLink $apiLink = null, ApiConsoleFormFactoryInterface $formFactory = null) - { - $this->request = $request; - $this->endpoint = $endpoint; - $this->handler = $handler; - $this->authorization = $authorization; - $this->apiLink = $apiLink; + public function __construct( + private IRequest $request, + private EndpointInterface $endpoint, + private ApiHandlerInterface $handler, + private ApiAuthorizationInterface $authorization, + private ?ApiLink $apiLink = null, + ?ApiConsoleFormFactoryInterface $formFactory = null + ) { $this->formFactory = $formFactory ?: new DefaultApiConsoleFormFactory(); } public function render(): void { - /** @var Template $template */ + /** @var DefaultTemplate $template */ $template = $this->getTemplate(); $template->setFile($this->getTemplateFilePath()); $template->add('handler', $this->handler); @@ -63,7 +52,7 @@ protected function createComponentConsoleForm(): Form { $form = $this->formFactory->create($this->request, $this->endpoint, $this->handler, $this->authorization, $this->apiLink); $form->setRenderer($this->getFormRenderer()); - $form->onSuccess[] = array($this, 'formSucceeded'); + $form->onSuccess[] = [$this, 'formSucceeded']; return $form; } @@ -97,7 +86,7 @@ public function formSucceeded(Form $form, ArrayHash $values): void $additionalValues['getFields'][$queryParamName] = $values[$queryParamName] ?? null; } elseif ($this->authorization instanceof HeaderApiKeyAuthentication) { $headerName = $this->authorization->getHeaderName(); - $additionalValues['headers'][] = $headerName . ':' . $values['header_api_key'] ?? null; + $additionalValues['headers'][] = $headerName . ':' . ($values['header_api_key'] ?? null); } elseif ($this->authorization instanceof CookieApiKeyAuthentication) { $cookieName = $this->authorization->getCookieName(); $additionalValues['cookieFields'][$cookieName] = $values['cookie_api_key'] ?? null; @@ -106,7 +95,7 @@ public function formSucceeded(Form $form, ArrayHash $values): void $consoleRequest = new ConsoleRequest($this->handler, $this->endpoint, $this->apiLink); $result = $consoleRequest->makeRequest($url, $method, $this->filterFormValues((array) $values), $additionalValues, $token); - /** @var Template $template */ + /** @var DefaultTemplate $template */ $template = $this->getTemplate(); $template->add('response', $result); @@ -115,12 +104,12 @@ public function formSucceeded(Form $form, ArrayHash $values): void } } - public function setFormRenderer(IFormRenderer $formRenderer): void + public function setFormRenderer(FormRenderer $formRenderer): void { $this->formRenderer = $formRenderer; } - private function getFormRenderer(): IFormRenderer + private function getFormRenderer(): FormRenderer { return $this->formRenderer ?: new BootstrapVerticalRenderer(); } @@ -135,6 +124,10 @@ private function getTemplateFilePath(): string return $this->templateFilePath ?: __DIR__ . '/console.latte'; } + /** + * @param mixed[] $values + * @return mixed[] + */ private function filterFormValues(array $values): array { foreach ($this->handler->params() as $param) { @@ -142,8 +135,10 @@ private function filterFormValues(array $values): array if ($values['do_not_send_empty_value_for_' . $key] === true && $values[$key] === '') { unset($values[$key]); } + unset($values['do_not_send_empty_value_for_' . $key]); } + return $values; } } diff --git a/src/Component/ApiListingControl.php b/src/Component/ApiListingControl.php index 4965a98..1157bdb 100644 --- a/src/Component/ApiListingControl.php +++ b/src/Component/ApiListingControl.php @@ -5,21 +5,24 @@ namespace Tomaj\NetteApi\Component; use Nette\Application\UI\Control; -use Nette\Bridges\ApplicationLatte\Template; -use Tomaj\NetteApi\ApiDecider; +use Nette\Bridges\ApplicationLatte\DefaultTemplate; use Tomaj\NetteApi\Api; +use Tomaj\NetteApi\ApiDecider; /** - * @method void onClick(string $method, int $version, string $package, ?string $apiAction) + * @method void onClick(string $method, string $version, string $package, ?string $apiAction) */ class ApiListingControl extends Control { /** @var ApiDecider */ private $apiDecider; - public $onClick = []; + /** + * @var array + */ + public array $onClick = []; - private $templateFilePath; + private ?string $templateFilePath = null; public function __construct(ApiDecider $apiDecider) { @@ -30,21 +33,21 @@ public function render(): void { $apis = $this->apiDecider->getApis(); - /** @var Template $template */ + /** @var DefaultTemplate $template */ $template = $this->getTemplate(); $template->add('apis', $this->groupApis($apis)); $template->setFile($this->getTemplateFilePath()); $template->render(); } - public function handleSelect(string $method, $version, string $package, ?string $apiAction = null): void + public function handleSelect(string $method, string $version, string $package, ?string $apiAction = null): void { $this->onClick($method, $version, $package, $apiAction); } /** * @param Api[] $handlers - * @return array + * @return array>. */ private function groupApis(array $handlers): array { @@ -54,8 +57,10 @@ private function groupApis(array $handlers): array if (!isset($versionHandlers[$endPoint->getVersion()])) { $versionHandlers[$endPoint->getVersion()] = []; } + $versionHandlers[$endPoint->getVersion()][] = $handler; } + return $versionHandlers; } diff --git a/src/Component/DefaultApiConsoleFormFactory.php b/src/Component/DefaultApiConsoleFormFactory.php index 7c48715..fb5fb50 100644 --- a/src/Component/DefaultApiConsoleFormFactory.php +++ b/src/Component/DefaultApiConsoleFormFactory.php @@ -20,6 +20,8 @@ class DefaultApiConsoleFormFactory implements ApiConsoleFormFactoryInterface { + private const HTTP_PORT = 80; + public function create( IRequest $request, EndpointInterface $endpoint, @@ -66,14 +68,18 @@ protected function getUrl(IRequest $request, EndpointInterface $endpoint, ?ApiLi if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { $scheme = $_SERVER['HTTP_X_FORWARDED_PROTO']; } + $port = ''; - if ($uri->scheme === 'http' && $uri->port !== 80) { + if ($uri->scheme === 'http' && $uri->port !== self::HTTP_PORT) { $port = ':' . $uri->port; } return $scheme . '://' . $uri->host . $port . '/api/' . $endpoint->getUrl(); } + /** + * @param array $defaults + */ protected function addAuthorization(Form $form, ApiAuthorizationInterface $authorization, array &$defaults): void { if ($authorization instanceof BearerTokenAuthorization) { diff --git a/src/EndpointIdentifier.php b/src/EndpointIdentifier.php index 43fae40..1f451c9 100644 --- a/src/EndpointIdentifier.php +++ b/src/EndpointIdentifier.php @@ -29,6 +29,7 @@ public function __construct(string $method, $version, string $package, ?string $ 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; @@ -41,7 +42,7 @@ public function getMethod(): string public function getVersion(): string { - return $this->version; + return (string) $this->version; } public function getPackage(): string @@ -54,11 +55,12 @@ public function getApiAction(): ?string if ($this->apiAction === '') { return null; } + return $this->apiAction; } public function getUrl(): string { - return "v{$this->version}/{$this->package}/{$this->apiAction}"; + return sprintf('v%s/%s/%s', $this->version, $this->package, $this->apiAction); } } diff --git a/src/Error/DefaultErrorHandler.php b/src/Error/DefaultErrorHandler.php index 9dcb512..56eb2cc 100644 --- a/src/Error/DefaultErrorHandler.php +++ b/src/Error/DefaultErrorHandler.php @@ -29,6 +29,7 @@ public function handle(Throwable $exception, array $params): JsonApiResponse } else { $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); } + return $response; } @@ -39,6 +40,7 @@ public function handleInputParams(array $errors): JsonApiResponse } else { $response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input']); } + return $response; } @@ -51,6 +53,7 @@ public function handleSchema(array $errors, array $params): JsonApiResponse } else { $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); } + return $response; } diff --git a/src/Error/ErrorHandlerInterface.php b/src/Error/ErrorHandlerInterface.php index 91974e4..10cfe99 100644 --- a/src/Error/ErrorHandlerInterface.php +++ b/src/Error/ErrorHandlerInterface.php @@ -4,9 +4,9 @@ namespace Tomaj\NetteApi\Error; +use Throwable; use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface; use Tomaj\NetteApi\Response\JsonApiResponse; -use Throwable; interface ErrorHandlerInterface { @@ -16,13 +16,12 @@ interface ErrorHandlerInterface public function handle(Throwable $exception, array $params): JsonApiResponse; /** - * @param array $errors - * @param array $params + * @param array $errors */ public function handleInputParams(array $errors): JsonApiResponse; /** - * @param array $errors + * @param array $errors * @param array $params */ public function handleSchema(array $errors, array $params): JsonApiResponse; diff --git a/src/Handlers/ApiHandlerInterface.php b/src/Handlers/ApiHandlerInterface.php index b2ed174..d4e9581 100644 --- a/src/Handlers/ApiHandlerInterface.php +++ b/src/Handlers/ApiHandlerInterface.php @@ -13,13 +13,11 @@ interface ApiHandlerInterface { /** * Summary of handler - short description of handler - * @return string */ public function summary(): string; /** * Description of handler - * @return string */ public function description(): string; @@ -32,13 +30,12 @@ public function params(): array; /** * Returns list of tags for handler - * @return array + * @return string[] */ public function tags(): array; /** * Marks handler as deprecated - * @return bool */ public function deprecated(): bool; @@ -46,9 +43,7 @@ public function deprecated(): bool; * Main handle method that will be executed when api * endpoint contected with this handler will be triggered * - * @param array $params - * - * @return ResponseInterface + * @param array $params */ public function handle(array $params): ResponseInterface; @@ -56,9 +51,7 @@ public function handle(array $params): ResponseInterface; * Set actual endpoint identifier to hnadler. * It is neccesary for link creation. * - * @param EndpointInterface $endpoint * - * @return void */ public function setEndpointIdentifier(EndpointInterface $endpoint): void; diff --git a/src/Handlers/ApiListingHandler.php b/src/Handlers/ApiListingHandler.php index 0d0be0e..df43e19 100644 --- a/src/Handlers/ApiListingHandler.php +++ b/src/Handlers/ApiListingHandler.php @@ -4,8 +4,8 @@ namespace Tomaj\NetteApi\Handlers; -use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Api; +use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Link\ApiLink; use Tomaj\NetteApi\Params\InputParam; use Tomaj\NetteApi\Response\JsonApiResponse; @@ -25,9 +25,6 @@ class ApiListingHandler extends BaseHandler /** * ApiListingHandler constructor. - * - * @param ApiDecider $apiDecider - * @param ApiLink $apiLink */ public function __construct(ApiDecider $apiDecider, ApiLink $apiLink) { @@ -41,17 +38,16 @@ public function __construct(ApiDecider $apiDecider, ApiLink $apiLink) */ public function handle(array $params): ResponseInterface { - $version = $this->getEndpoint()->getVersion(); - $endpoints = $this->getApiList($version); + $version = $this->getEndpoint()?->getVersion(); + $endpoints = $this->getApiList($version ?? ''); return new JsonApiResponse(200, ['endpoints' => $endpoints]); } /** * Create handler list for specified version * - * @param integer $version * - * @return array + * @return array */ private function getApiList(string $version): array { @@ -75,13 +71,13 @@ private function getApiList(string $version): array /** * Create array with params for specified handler * - * @param ApiHandlerInterface $handler * - * @return array + * @return array{type: string, key: string, is_required: bool, available_values?: non-empty-array} */ private function createParamsList(ApiHandlerInterface $handler): array { - return array_map(function (InputParam $param) { + /** @phpstan-ignore-next-line */ + return array_map(function (InputParam $param): array { $parameter = [ 'type' => $param->getType(), 'key' => $param->getKey(), @@ -90,6 +86,7 @@ private function createParamsList(ApiHandlerInterface $handler): array if ($param->getAvailableValues()) { $parameter['available_values'] = $param->getAvailableValues(); } + return $parameter; }, $handler->params()); } diff --git a/src/Handlers/BaseHandler.php b/src/Handlers/BaseHandler.php index 727fc7e..6d83380 100644 --- a/src/Handlers/BaseHandler.php +++ b/src/Handlers/BaseHandler.php @@ -29,7 +29,7 @@ abstract class BaseHandler implements ApiHandlerInterface */ protected $linkGenerator; - public function __construct(ScopeFactoryInterface $scopeFactory = null) + public function __construct(?ScopeFactoryInterface $scopeFactory = null) { $this->fractal = new Manager($scopeFactory); } @@ -60,6 +60,7 @@ public function params(): array /** * {@inheritdoc} + * @return string[] */ public function tags(): array { @@ -87,6 +88,7 @@ protected function getFractal(): Manager if (!$this->fractal) { throw new InvalidStateException("Fractal manager isn't initialized. Did you call parent::__construct() in your handler constructor?"); } + return $this->fractal; } @@ -106,9 +108,7 @@ final public function getEndpoint(): ?EndpointInterface /** * Set link generator to handler * - * @param LinkGenerator $linkGenerator * - * @return self */ final public function setupLinkGenerator(LinkGenerator $linkGenerator): self { @@ -119,29 +119,31 @@ final public function setupLinkGenerator(LinkGenerator $linkGenerator): self /** * Create link to actual handler endpoint * - * @param array $params + * @param array $params * - * @return string * @throws InvalidLinkException if handler doesn't have linkgenerator or endpoint */ final public function createLink(array $params = []): string { if (!$this->linkGenerator) { - throw new InvalidStateException("You have setupLinkGenerator for this handler if you want to generate link in this handler"); + throw new InvalidStateException('You have setupLinkGenerator for this handler if you want to generate link in this handler'); } + if (!$this->endpoint) { - throw new InvalidStateException("You have setEndpoint() for this handler if you want to generate link in this handler"); + throw new InvalidStateException('You have setEndpoint() for this handler if you want to generate link in this handler'); } + $params = array_merge([ 'version' => $this->endpoint->getVersion(), 'package' => $this->endpoint->getPackage(), - 'apiAction' => $this->endpoint->getApiAction() + 'apiAction' => $this->endpoint->getApiAction(), ], $params); - return $this->linkGenerator->link('Api:Api:default', $params); + return $this->linkGenerator->link('Api:Api:default', $params) ?: ''; } /** * {@inheritdoc} + * @param array $params */ abstract public function handle(array $params): ResponseInterface; } diff --git a/src/Handlers/CorsPreflightHandler.php b/src/Handlers/CorsPreflightHandler.php index 1213798..178783c 100644 --- a/src/Handlers/CorsPreflightHandler.php +++ b/src/Handlers/CorsPreflightHandler.php @@ -10,10 +10,14 @@ class CorsPreflightHandler extends BaseHandler implements CorsPreflightHandlerInterface { - private $response; + private Response $response; - private $headers = []; + /** @var array */ + private array $headers = []; + /** + * @param array $headers + */ public function __construct( Response $response, array $headers = [ @@ -28,6 +32,9 @@ public function __construct( $this->headers = $headers; } + /** + * @param array $params + */ public function handle(array $params): ResponseInterface { foreach ($this->headers as $name => $values) { @@ -36,6 +43,7 @@ public function handle(array $params): ResponseInterface $this->response->addHeader($name, $value); } } + return new JsonApiResponse(Response::S200_OK, []); } } diff --git a/src/Handlers/CorsPreflightHandlerInterface.php b/src/Handlers/CorsPreflightHandlerInterface.php index e4e87e6..5019123 100644 --- a/src/Handlers/CorsPreflightHandlerInterface.php +++ b/src/Handlers/CorsPreflightHandlerInterface.php @@ -4,10 +4,6 @@ namespace Tomaj\NetteApi\Handlers; -use Nette\Http\Response; -use Tomaj\NetteApi\Response\JsonApiResponse; -use Tomaj\NetteApi\Response\ResponseInterface; - interface CorsPreflightHandlerInterface extends ApiHandlerInterface { diff --git a/src/Handlers/DefaultHandler.php b/src/Handlers/DefaultHandler.php index 598881e..6fba838 100644 --- a/src/Handlers/DefaultHandler.php +++ b/src/Handlers/DefaultHandler.php @@ -12,6 +12,7 @@ class DefaultHandler extends BaseHandler { /** * {@inheritdoc} + * @param array $params */ public function handle(array $params): ResponseInterface { diff --git a/src/Handlers/EchoHandler.php b/src/Handlers/EchoHandler.php index 0a50537..1636c42 100644 --- a/src/Handlers/EchoHandler.php +++ b/src/Handlers/EchoHandler.php @@ -23,6 +23,7 @@ public function params(): array /** * {@inheritdoc} + * @param array $params */ public function handle(array $params): ResponseInterface { diff --git a/src/Handlers/OpenApiHandler.php b/src/Handlers/OpenApiHandler.php index 2cd69d8..f8291cc 100644 --- a/src/Handlers/OpenApiHandler.php +++ b/src/Handlers/OpenApiHandler.php @@ -40,16 +40,15 @@ class OpenApiHandler extends BaseHandler /** @var Request */ private $request; + /** @var array */ private $initData = []; + /** @var array */ private $definitions = []; /** * OpenApiHandler constructor. - * @param ApiDecider $apiDecider - * @param ApiLink $apiLink - * @param Request $request - * @param array $initData - structured data for initialization response + * @param array $initData - structured data for initialization response */ public function __construct( ApiDecider $apiDecider, @@ -64,12 +63,16 @@ public function __construct( $this->initData = $initData; } + /** + * @return InputParam[] + */ public function params(): array { $availableFormats = ['json']; if (class_exists(Yaml::class)) { $availableFormats[] = 'yaml'; } + return [ (new GetInputParam('format'))->setAvailableValues($availableFormats)->setDescription('Response format'), ]; @@ -85,6 +88,7 @@ public function description(): string /** * {@inheritdoc} + * @return string[] */ public function tags(): array { @@ -93,11 +97,12 @@ public function tags(): array /** * {@inheritdoc} + * @param array $params */ public function handle(array $params): ResponseInterface { - $version = $this->getEndpoint()->getVersion(); - $apis = $this->getApis($version); + $version = $this->getEndpoint()?->getVersion(); + $apis = $this->getApis($version ?: ''); $scheme = $this->request->getUrl()->getScheme(); $baseUrl = $this->request->getUrl()->getHostUrl(); $basePath = $this->getBasePath($apis, $baseUrl); @@ -113,6 +118,7 @@ public function handle(array $params): ResponseInterface ]; continue; } + if ($authorization instanceof BearerTokenAuthorization) { $securitySchemes['Bearer'] = [ 'type' => 'http', @@ -120,6 +126,7 @@ public function handle(array $params): ResponseInterface ]; continue; } + if ($authorization instanceof QueryApiKeyAuthentication) { $queryParamName = $authorization->getQueryParamName(); $securitySchemes[$this->normalizeSecuritySchemeName('query', $queryParamName)] = [ @@ -129,6 +136,7 @@ public function handle(array $params): ResponseInterface ]; continue; } + if ($authorization instanceof HeaderApiKeyAuthentication) { $headerName = $authorization->getHeaderName(); $securitySchemes[$this->normalizeSecuritySchemeName('header', $headerName)] = [ @@ -138,6 +146,7 @@ public function handle(array $params): ResponseInterface ]; continue; } + if ($authorization instanceof CookieApiKeyAuthentication) { $cookieName = $authorization->getCookieName(); $securitySchemes[$this->normalizeSecuritySchemeName('cookie', $cookieName)] = [ @@ -152,7 +161,7 @@ public function handle(array $params): ResponseInterface $data = [ 'openapi' => '3.0.0', 'info' => [ - 'version' => (string)$version, + 'version' => $version, 'title' => 'Nette API', ], 'servers' => [ @@ -224,9 +233,13 @@ public function handle(array $params): ResponseInterface if ($params['format'] === 'yaml') { return new TextApiResponse(IResponse::S200_OK, Yaml::dump($data, PHP_INT_MAX, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE)); } + return new JsonApiResponse(IResponse::S200_OK, $data); } + /** + * @return Api[] + */ private function getApis(string $version): array { return array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) { @@ -236,9 +249,7 @@ private function getApis(string $version): array /** * @param Api[] $versionApis - * @param string $baseUrl - * @param string $basePath - * @return array + * @return array * @throws InvalidLinkException */ private function getPaths(array $versionApis, string $baseUrl, string $basePath): array @@ -270,7 +281,7 @@ private function getPaths(array $versionApis, string $baseUrl, string $basePath) 'schema' => [ '$ref' => '#/components/schemas/ErrorWrongInput', ], - ] + ], ], ]; } @@ -311,28 +322,34 @@ private function getPaths(array $versionApis, string $baseUrl, string $basePath) 'application/json; charset=utf-8' => [ 'schema' => $schema, ], - ] + ], ]; if (!empty($examples = $output->getExamples())) { if (count($examples) === 1) { - $example = is_array($output->getExample())? $output->getExample() : json_decode($output->getExample(), true); + $example = is_array($output->getExample()) ? $output->getExample() : json_decode($output->getExample(), true); + /** @phpstan-ignore-next-line */ $responses[$output->getCode()]['content']['application/json; charset=utf-8']['example'] = $example; } else { foreach ($examples as $exampleKey => $example) { - $example = is_array($example)? $example : json_decode($example, true); + $example = is_array($example) ? $example : json_decode($example, true); + /** @phpstan-ignore-next-line */ $responses[$output->getCode()]['content']['application/json; charset=utf-8']['examples'][$exampleKey] = $example; } } } } else { if (!isset($responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']['oneOf'])) { + /** @phpstan-ignore-next-line */ $tmp = $responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']; unset($responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']); + /** @phpstan-ignore-next-line */ $responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema'] = [ 'oneOf' => [], ]; $responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']['oneOf'][] = $tmp; } + + /** @phpstan-ignore-next-line */ $responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']['oneOf'][] = $schema; } } @@ -345,9 +362,9 @@ private function getPaths(array $versionApis, string $baseUrl, string $basePath) 'description' => $output->getDescription(), 'schema' => [ 'type' => 'string', - ] + ], ], - ] + ], ]; } } @@ -391,45 +408,52 @@ private function getPaths(array $versionApis, string $baseUrl, string $basePath) ], ]; } + $settings['responses'] = $responses; $list[$path][strtolower($api->getEndpoint()->getMethod())] = $settings; } + return $list; } + /** + * @param Api[] $apis + */ private function getBasePath(array $apis, string $baseUrl): string { $basePath = ''; foreach ($apis as $handler) { $basePath = $this->getLongestCommonSubstring($basePath, $this->apiLink->link($handler->getEndpoint())); } + return rtrim(str_replace($baseUrl, '', $basePath), '/'); } - private function getLongestCommonSubstring($path1, $path2) + private function getLongestCommonSubstring(?string $path1, string $path2): string { if ($path1 === null) { return $path2; } + $commonSubstring = ''; $shortest = min(strlen($path1), strlen($path2)); for ($i = 0; $i <= $shortest; ++$i) { if (substr($path1, 0, $i) !== substr($path2, 0, $i)) { break; } + $commonSubstring = substr($path1, 0, $i); } + return $commonSubstring; } /** * Create array with params for specified handler * - * @param ApiHandlerInterface $handler - * - * @return array + * @return array> */ - private function createParamsList(ApiHandlerInterface $handler) + private function createParamsList(ApiHandlerInterface $handler): array { $parameters = []; foreach ($handler->params() as $param) { @@ -449,10 +473,12 @@ private function createParamsList(ApiHandlerInterface $handler) if ($param->isMulti()) { $schema['items'] = ['type' => 'string']; } + $descriptionParts = []; if ($param->getDescription()) { $descriptionParts[] = $param->getDescription(); } + $availableValues = $param->getAvailableValues(); if ($availableValues) { $schema['enum'] = array_keys($availableValues); @@ -462,6 +488,7 @@ private function createParamsList(ApiHandlerInterface $handler) } } } + $parameter['schema'] = $schema; if ($descriptionParts !== []) { $parameter['description'] = implode("\n", $descriptionParts); @@ -473,9 +500,13 @@ private function createParamsList(ApiHandlerInterface $handler) $parameters[] = $parameter; } + return $parameters; } + /** + * @return array|null + */ private function createRequestBody(ApiHandlerInterface $handler) { $requestBody = [ @@ -490,10 +521,10 @@ private function createRequestBody(ApiHandlerInterface $handler) $schema = json_decode($param->getSchema(), true); if (!empty($examples = $param->getExamples())) { if (count($examples) === 1) { - $schema['example'] = is_array($param->getExample())? $param->getExample() : json_decode($param->getExample(), true); + $schema['example'] = is_array($param->getExample()) ? $param->getExample() : json_decode($param->getExample(), true); } else { foreach ($examples as $exampleKey => $example) { - $schema['examples'][$exampleKey] = is_array($example)? $example : json_decode($example, true); + $schema['examples'][$exampleKey] = is_array($example) ? $example : json_decode($example, true); } } } @@ -504,6 +535,7 @@ private function createRequestBody(ApiHandlerInterface $handler) 'schema' => $this->transformSchema($schema), ]; } + if ($param instanceof RawInputParam) { $schema = [ 'type' => 'string', @@ -521,6 +553,7 @@ private function createRequestBody(ApiHandlerInterface $handler) 'schema' => $schema, ]; } + if ($param->getType() === InputParam::TYPE_POST || $param->getType() === InputParam::TYPE_PUT) { $property = [ 'type' => $param->isMulti() ? 'array' : 'string', @@ -528,10 +561,12 @@ private function createRequestBody(ApiHandlerInterface $handler) if ($param->isMulti()) { $property['items'] = ['type' => 'string']; } + $descriptionParts = []; if ($param->getDescription()) { $descriptionParts[] = $param->getDescription(); } + $availableValues = $param->getAvailableValues(); if ($availableValues) { $property['enum'] = array_keys($availableValues); @@ -598,17 +633,27 @@ private function createRequestBody(ApiHandlerInterface $handler) return $result ?: null; } + /** + * @param int|string $type + * @return string + */ private function createIn($type) { if ($type == InputParam::TYPE_GET) { return 'query'; } + if ($type == InputParam::TYPE_COOKIE) { return 'cookie'; } + return 'body'; } + /** + * @param array $schema + * @return array + */ private function transformSchema(array $schema) { OpenApiTransform::transformTypes($schema); @@ -617,16 +662,23 @@ private function transformSchema(array $schema) foreach ($schema['definitions'] as $name => $definition) { $this->addDefinition($name, $this->transformSchema($definition)); } + unset($schema['definitions']); } - return json_decode(str_replace('#/definitions/', '#/components/schemas/', json_encode($schema, JSON_UNESCAPED_SLASHES)), true); + + return json_decode(str_replace('#/definitions/', '#/components/schemas/', json_encode($schema, JSON_UNESCAPED_SLASHES) ?: ''), true); } - private function addDefinition($name, $definition) + /** + * @param string $name + * @param array $definition + */ + private function addDefinition($name, $definition): void { if (isset($this->definitions[$name]) && $this->definitions[$name] !== $definition) { throw new InvalidArgumentException('Definition with name ' . $name . ' already exists. Rename it.'); } + $this->definitions[$name] = $definition; } diff --git a/src/Link/ApiLink.php b/src/Link/ApiLink.php index c9a8e1b..0cdea93 100644 --- a/src/Link/ApiLink.php +++ b/src/Link/ApiLink.php @@ -15,8 +15,6 @@ class ApiLink /** * Create ApiLink - * - * @param LinkGenerator $linkGenerator */ public function __construct(LinkGenerator $linkGenerator) { @@ -26,19 +24,16 @@ public function __construct(LinkGenerator $linkGenerator) /** * Create link to specified api endpoint * - * @param EndpointInterface $endpoint - * @param array $params - * - * @return string + * @param array $params * @throws InvalidLinkException */ - public function link(EndpointInterface $endpoint, $params = []) + public function link(EndpointInterface $endpoint, $params = []): string { $params = array_merge([ 'version' => $endpoint->getVersion(), 'package' => $endpoint->getPackage(), - 'apiAction' => $endpoint->getApiAction() + 'apiAction' => $endpoint->getApiAction(), ], $params); - return $this->linkGenerator->link('Api:Api:default', $params); + return $this->linkGenerator->link('Api:Api:default', $params) ?: ''; } } diff --git a/src/Link/ApiLinkMacro.php b/src/Link/ApiLinkMacro.php deleted file mode 100644 index 2e63f7b..0000000 --- a/src/Link/ApiLinkMacro.php +++ /dev/null @@ -1,58 +0,0 @@ - 'My title', 'data-foo' => 'bar']} - */ -class ApiLinkMacro extends MacroSet -{ - public static function install(Compiler $compiler) - { - $macroSet = new static($compiler); - $macroSet->addMacro('apiLink', [self::class, 'start']); - } - - public static function start(MacroNode $node, PhpWriter $writer) - { - $args = array_map('trim', explode(',', $node->args, 5)); - - if (count($args) < 3) { - $message = "Invalid link destination, too few arguments."; - if (!Debugger::$productionMode) { - throw new InvalidLinkException($message); - } - Debugger::log($message, Debugger::EXCEPTION); - return ''; - } - - $arguments = [ - 'method' => self::addQuotes($args[0]), - 'version' => $args[1], - 'package' => self::addQuotes($args[2]), - 'action' => isset($args[3]) ? self::addQuotes($args[3]) : 'null', - 'params' => $args[4] ?? '[]', - ]; - - return $writer->write('echo ($this->filters->apiLink)((new Tomaj\NetteApi\EndpointIdentifier(' . - $arguments['method'] . ', ' . - $arguments['version'] . ', ' . - $arguments['package'] . ', ' . - $arguments['action'] . ')), ' . $arguments['params'] . ')'); - } - - private static function addQuotes($string) - { - return '"' . trim($string, "'\"") . '"'; - } -} diff --git a/src/Link/ApiLinkNode.php b/src/Link/ApiLinkNode.php index 1699971..bfe9ac8 100644 --- a/src/Link/ApiLinkNode.php +++ b/src/Link/ApiLinkNode.php @@ -4,6 +4,7 @@ namespace Tomaj\NetteApi\Link; +use Generator; use Latte\Compiler\Nodes\Php\Expression\ArrayNode; use Latte\Compiler\Nodes\StatementNode; use Latte\Compiler\PrintContext; @@ -37,8 +38,7 @@ public function print(PrintContext $context): string return $context->format('echo ($this->filters->apiLink)(new Tomaj\NetteApi\EndpointIdentifier(%args), %args);', $this->endpointArgs, $this->endpointParams); } - - public function &getIterator(): \Generator + public function &getIterator(): Generator { yield $this->endpointArgs; yield $this->endpointParams; diff --git a/src/Misc/ConsoleRequest.php b/src/Misc/ConsoleRequest.php index f9ab891..2447749 100644 --- a/src/Misc/ConsoleRequest.php +++ b/src/Misc/ConsoleRequest.php @@ -13,6 +13,10 @@ class ConsoleRequest { + public const DEFAULT_TIMEOUT = 30; + + public const ELAPSED_MILLISECONDS = 1000; + /** @var ApiHandlerInterface */ private $handler; @@ -29,9 +33,13 @@ public function __construct(ApiHandlerInterface $handler, ?EndpointInterface $en $this->apiLink = $apiLink; } + /** + * @param array $values + * @param array $additionalValues + */ public function makeRequest(string $url, string $method, array $values, array $additionalValues = [], ?string $token = null): ConsoleResponse { - list($postFields, $getFields, $cookieFields, $rawPost, $putFields) = $this->processValues($values); + [$postFields, $getFields, $cookieFields, $rawPost, $putFields] = $this->processValues($values); $postFields = array_merge($postFields, $additionalValues['postFields'] ?? []); $getFields = array_merge($getFields, $additionalValues['getFields'] ?? []); @@ -44,10 +52,10 @@ public function makeRequest(string $url, string $method, array $values, array $a if ($this->endpoint && $this->apiLink) { $url = $this->apiLink->link($this->endpoint, $getFields); - } elseif (count($getFields)) { + } elseif ($getFields !== []) { $parts = []; foreach ($getFields as $key => $value) { - $parts[] = "$key=$value"; + $parts[] = sprintf('%s=%s', $key, $value); } $parsedUrl = parse_url($url); @@ -56,11 +64,12 @@ public function makeRequest(string $url, string $method, array $values, array $a } $putRawPost = null; - if (count($putFields)) { + if ($putFields !== []) { $parts = []; foreach ($putFields as $key => $value) { - $parts[] = "$key=$value"; + $parts[] = sprintf('%s=%s', $key, $value); } + $putRawPost = implode('&', $parts); } @@ -73,7 +82,7 @@ public function makeRequest(string $url, string $method, array $values, array $a curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_VERBOSE, false); - curl_setopt($curl, CURLOPT_TIMEOUT, $additionalValues['timeout'] ?? 30); + curl_setopt($curl, CURLOPT_TIMEOUT, $additionalValues['timeout'] ?? self::DEFAULT_TIMEOUT); curl_setopt($curl, CURLOPT_HEADER, true); if (count($postFields) || $rawPost || $putRawPost !== null) { @@ -82,16 +91,19 @@ public function makeRequest(string $url, string $method, array $values, array $a } $headers = $additionalValues['headers'] ?? []; - if (count($cookieFields)) { + if ($cookieFields !== []) { $parts = []; foreach ($cookieFields as $key => $value) { - $parts[] = "$key=$value"; + $parts[] = sprintf('%s=%s', $key, $value); } - $headers[] = "Cookie: " . implode('&', $parts); + + $headers[] = 'Cookie: ' . implode('&', $parts); } + if ($token !== null && $token !== false) { $headers[] = 'Authorization: Bearer ' . $token; } + if (count($headers)) { curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); } @@ -99,7 +111,7 @@ public function makeRequest(string $url, string $method, array $values, array $a $basicAuthUsername = $values['basic_authentication_username'] ?? null; $basicAuthPassword = $values['basic_authentication_password'] ?? null; if ($basicAuthUsername && $basicAuthPassword) { - curl_setopt($curl, CURLOPT_USERPWD, "$basicAuthUsername:$basicAuthPassword"); + curl_setopt($curl, CURLOPT_USERPWD, sprintf('%s:%s', $basicAuthUsername, $basicAuthPassword)); } $consoleResponse = new ConsoleResponse( @@ -113,13 +125,14 @@ public function makeRequest(string $url, string $method, array $values, array $a ); $response = curl_exec($curl); - $elapsed = intval((microtime(true) - $startTime) * 1000); + $elapsed = (int) ((microtime(true) - $startTime) * self::ELAPSED_MILLISECONDS); if ($response === false) { $response = ''; } $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $response = (string) $response; $responseHeaders = substr($response, 0, $headerSize); $responseBody = substr($response, $headerSize); @@ -138,18 +151,21 @@ public function makeRequest(string $url, string $method, array $values, array $a /** * Process given values to POST and GET fields * - * @param array $values - * - * @return array + * @param array $values + * @return array{0:array,1:array,2:array,3:string|null,4:array} */ private function processValues(array $values): array { $params = $this->handler->params(); + /** @var array $postFields */ $postFields = []; - $rawPost = isset($values['post_raw']) ? $values['post_raw'] : null; + $rawPost = $values['post_raw'] ?? null; + /** @var array $getFields */ $getFields = []; + /** @var array $putFields */ $putFields = []; + /** @var array $cookieFields */ $cookieFields = []; foreach ($values as $key => $value) { @@ -165,7 +181,7 @@ private function processValues(array $values): array } if ($param->isMulti()) { - if (in_array($param->getType(), [InputParam::TYPE_POST, InputParam::TYPE_FILE])) { + if (in_array($param->getType(), [InputParam::TYPE_POST, InputParam::TYPE_FILE], true)) { $postFields[$key][] = $valueData; } elseif ($param->getType() === InputParam::TYPE_PUT) { $putFields[$key][] = $valueData; @@ -175,7 +191,7 @@ private function processValues(array $values): array $getFields[$key][] = urlencode((string)$valueData); } } else { - if (in_array($param->getType(), [InputParam::TYPE_POST, InputParam::TYPE_FILE])) { + if (in_array($param->getType(), [InputParam::TYPE_POST, InputParam::TYPE_FILE], true)) { $postFields[$key] = $valueData; } elseif ($param->getType() === InputParam::TYPE_PUT) { $putFields[$key] = $valueData; @@ -219,9 +235,14 @@ private function processParam(ParamInterface $param, string $key, $value) return $valueData; } + return null; } + /** + * @param array $values + * @return array + */ private function normalizeValues(array $values): array { $result = []; @@ -233,10 +254,11 @@ private function normalizeValues(array $values): array foreach ($value as $innerKey => $innerValue) { if ($innerValue !== '' && $innerValue !== null) { - $result[$key . "[" . $innerKey . "]"] = $innerValue; + $result[$key . '[' . $innerKey . ']'] = $innerValue; } } } + return $result; } } diff --git a/src/Misc/ConsoleResponse.php b/src/Misc/ConsoleResponse.php index e231ef9..1b2144c 100644 --- a/src/Misc/ConsoleResponse.php +++ b/src/Misc/ConsoleResponse.php @@ -6,34 +6,44 @@ class ConsoleResponse { - private $postFields; + /** @var array */ + private array $postFields; - private $rawPost; + private ?string $rawPost; - private $getFields; + /** @var array */ + private array $getFields; - private $cookieFields; + /** @var array */ + private array $cookieFields; - private $url; + private string $url; - private $method; + private string $method; - private $headers; + /** @var array */ + private array $headers; - private $responseCode; + private ?int $responseCode = null; - private $responseBody; + private ?string $responseBody = null; - private $responseHeaders; + private ?string $responseHeaders = null; - private $responseTime; + private ?int $responseTime = null; - private $isError = false; + private bool $isError = false; - private $errorNumber; + private ?int $errorNumber = null; - private $errorMessage; + private ?string $errorMessage = null; + /** + * @param array $postFields + * @param array $getFields + * @param array $cookieFields + * @param array $headers + */ public function __construct(string $url, string $method, array $postFields = [], array $getFields = [], array $cookieFields = [], array $headers = [], ?string $rawPost = null) { $this->url = $url; @@ -71,6 +81,9 @@ public function getMethod(): string return $this->method; } + /** + * @return array + */ public function getPostFields(): array { return $this->postFields; @@ -81,16 +94,25 @@ public function getRawPost(): ?string return $this->rawPost; } + /** + * @return array + */ public function getGetFields(): array { return $this->getFields; } + /** + * @return array + */ public function getCookieFields(): array { return $this->cookieFields; } + /** + * @return array + */ public function getHeaders(): array { return $this->headers; @@ -106,7 +128,7 @@ public function getResponseCode(): ?int return $this->responseCode; } - public function getResponseBody(): string + public function getResponseBody(): ?string { return $this->responseBody; } @@ -117,11 +139,13 @@ public function getFormattedJsonBody(): string if ($body === null) { return ''; } + $decoded = json_decode($body); if ($decoded) { $body = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } - return $body; + + return $body ?: ''; } public function getResponseHeaders(): ?string @@ -129,17 +153,17 @@ public function getResponseHeaders(): ?string return $this->responseHeaders; } - public function getResponseTime(): int + public function getResponseTime(): ?int { return $this->responseTime; } - public function getErrorNumber(): int + public function getErrorNumber(): ?int { return $this->errorNumber; } - public function getErrorMessage(): string + public function getErrorMessage(): ?string { return $this->errorMessage; } diff --git a/src/Misc/IpDetector.php b/src/Misc/IpDetector.php index 36bea48..e37b780 100644 --- a/src/Misc/IpDetector.php +++ b/src/Misc/IpDetector.php @@ -20,6 +20,7 @@ public function getRequestIp(): string } else { $ip = 'cli'; } + return $ip; } } diff --git a/src/Misc/IpDetectorInterface.php b/src/Misc/IpDetectorInterface.php index 806fd11..a5ea8c8 100644 --- a/src/Misc/IpDetectorInterface.php +++ b/src/Misc/IpDetectorInterface.php @@ -8,8 +8,6 @@ interface IpDetectorInterface { /** * Get actual request IP. - * - * @return string */ public function getRequestIp(): string; } diff --git a/src/Misc/OpenApiTransform.php b/src/Misc/OpenApiTransform.php index 6afbfdf..32ea9cc 100644 --- a/src/Misc/OpenApiTransform.php +++ b/src/Misc/OpenApiTransform.php @@ -7,6 +7,7 @@ class OpenApiTransform { /** + * @param array $schema * @param string|int|null $parent */ public static function transformTypes(array &$schema, $parent = null): void @@ -14,15 +15,17 @@ public static function transformTypes(array &$schema, $parent = null): void foreach ($schema as $key => &$value) { if ($key === 'type' && is_array($value) && $parent !== 'properties') { if (count($value) > 1 && in_array('null', $value, true)) { - unset($value[array_search('null', $value)]); + unset($value[array_search('null', $value, true)]); $schema['nullable'] = true; } + if (count($value) === 1) { $value = implode(',', $value); } elseif (count($value) > 1) { foreach ($schema['type'] as $type) { $schema['oneOf'][] = ['type' => $type]; } + unset($schema['type']); } } elseif (is_array($value)) { diff --git a/src/Misc/StaticIpDetector.php b/src/Misc/StaticIpDetector.php index 2287b33..bf7150a 100644 --- a/src/Misc/StaticIpDetector.php +++ b/src/Misc/StaticIpDetector.php @@ -12,8 +12,6 @@ class StaticIpDetector implements IpDetectorInterface /** * Create Static Ip Detector * Ip that will be in constructor will return as actual request IP. - * - * @param string $ip */ public function __construct(string $ip) { diff --git a/src/Misc/StaticTokenRepository.php b/src/Misc/StaticTokenRepository.php index a9aa642..e60a464 100644 --- a/src/Misc/StaticTokenRepository.php +++ b/src/Misc/StaticTokenRepository.php @@ -10,11 +10,7 @@ class StaticTokenRepository implements BearerTokenRepositoryInterface { /** - * array - */ - private $validTokens = []; - - /** + * * Create static bearer token repository. * You can pass multiple tokens that will be available for your api. * Format is associtive array where key is token string and value is IP range @@ -26,12 +22,10 @@ class StaticTokenRepository implements BearerTokenRepositoryInterface * ['asfoihegoihregoihrhgrehg' => '127.0.0.1', 'asfo9uyewtoiyewgt4ty4r' => '*'] * * @see BearerTokenAuthorization#isValidIp for all available Ip range formats - * - * @param array $validTokens + * @param array $validTokens Array of valid tokens as keys and optional IP restrictions as values */ - public function __construct($validTokens = []) + public function __construct(private array $validTokens = []) { - $this->validTokens = $validTokens; } /** @@ -39,7 +33,7 @@ public function __construct($validTokens = []) */ public function validToken(string $token): bool { - return in_array($token, array_keys($this->validTokens)); + return in_array($token, array_keys($this->validTokens), true); } /** @@ -47,9 +41,6 @@ public function validToken(string $token): bool */ public function ipRestrictions(string $token): ?string { - if (isset($this->validTokens[$token])) { - return $this->validTokens[$token]; - } - return null; + return $this->validTokens[$token] ?? null; } } diff --git a/src/Misc/TokenRepositoryInterface.php b/src/Misc/TokenRepositoryInterface.php index 5615718..2ca735e 100644 --- a/src/Misc/TokenRepositoryInterface.php +++ b/src/Misc/TokenRepositoryInterface.php @@ -8,9 +8,6 @@ interface TokenRepositoryInterface { /** * Return true if token is valid, otherwise return false - * - * @param string $token - * @return bool */ public function validToken(string $token): bool; @@ -23,9 +20,7 @@ public function validToken(string $token): bool; * '156.26.252/32' - access from ip range * false - if token doesn't exists * - * @param string $token * - * @return string|null */ public function ipRestrictions(string $token): ?string; } diff --git a/src/Output/AbstractOutput.php b/src/Output/AbstractOutput.php index ed2a0f2..b0602a9 100644 --- a/src/Output/AbstractOutput.php +++ b/src/Output/AbstractOutput.php @@ -6,12 +6,12 @@ abstract class AbstractOutput implements OutputInterface { - protected $code; + protected int $code; - protected $description; + protected string $description; - /** @var array */ - protected $examples = []; + /** @var array */ + protected array $examples = []; public function __construct(int $code, string $description = '') { @@ -32,7 +32,6 @@ public function getDescription(): string /** * @param string $name Example name * @param mixed $example Example - * @return Self */ public function addExample(string $name, $example): self { @@ -43,12 +42,11 @@ public function addExample(string $name, $example): self /** * Set default example * @param mixed $example - * @return self * @deprecated Use addExample instead */ public function setExample($example): self { - $this->examples["default"] = $example; + $this->examples['default'] = $example; return $this; } @@ -61,12 +59,13 @@ public function getExample() if (empty($this->examples)) { return null; } + return reset($this->examples); } /** * Returns all examples - * @return array + * @return array */ public function getExamples(): array { diff --git a/src/Output/Configurator/EnvConfigurator.php b/src/Output/Configurator/EnvConfigurator.php index 51d07f4..8dc5cf9 100644 --- a/src/Output/Configurator/EnvConfigurator.php +++ b/src/Output/Configurator/EnvConfigurator.php @@ -7,6 +7,7 @@ class EnvConfigurator implements ConfiguratorInterface { private $envVariable = 'APP_ENV'; + private $productionValue = 'production'; /** @@ -25,6 +26,7 @@ public function validateSchema(): bool if ($appEnv === $this->productionValue) { return false; } + return true; } @@ -34,6 +36,7 @@ public function showErrorDetail(): bool if ($appEnv === $this->productionValue) { return false; } + return true; } } diff --git a/src/Output/Configurator/QueryConfigurator.php b/src/Output/Configurator/QueryConfigurator.php index 0a4fba1..9203e83 100644 --- a/src/Output/Configurator/QueryConfigurator.php +++ b/src/Output/Configurator/QueryConfigurator.php @@ -9,8 +9,10 @@ class QueryConfigurator implements ConfiguratorInterface { private $schemaValidateParam = 'schema_validate'; + private $errorDetailParam = 'error_detail'; - public $request = null; + + public Request $request; /** * @param string $schemaValidateParam Name of get parameter to enable schema validation diff --git a/src/Output/JsonOutput.php b/src/Output/JsonOutput.php index 38bcf68..3dc7317 100644 --- a/src/Output/JsonOutput.php +++ b/src/Output/JsonOutput.php @@ -25,11 +25,12 @@ public function validate(ResponseInterface $response): ValidationResultInterface if (!$response instanceof JsonApiResponse) { return new ValidationResult(ValidationResult::STATUS_ERROR); } + if ($this->code !== $response->getCode()) { return new ValidationResult(ValidationResult::STATUS_ERROR, ['Response code doesn\'t match']); } - $value = json_decode(json_encode($response->getPayload())); + $value = json_decode(json_encode($response->getPayload()) ?: ''); $schemaValidator = new JsonSchemaValidator(); return $schemaValidator->validate($value, $this->schema); diff --git a/src/Output/RedirectOutput.php b/src/Output/RedirectOutput.php index 0613c75..c38e4d2 100644 --- a/src/Output/RedirectOutput.php +++ b/src/Output/RedirectOutput.php @@ -16,9 +16,11 @@ public function validate(ResponseInterface $response): ValidationResultInterface if (!$response instanceof RedirectResponse) { return new ValidationResult(ValidationResult::STATUS_ERROR); } + if ($this->code !== $response->getCode()) { return new ValidationResult(ValidationResult::STATUS_ERROR, ['Response code doesn\'t match']); } + return new ValidationResult(ValidationResult::STATUS_OK); } } diff --git a/src/Params/CookieInputParam.php b/src/Params/CookieInputParam.php index acdd90f..7fb4228 100644 --- a/src/Params/CookieInputParam.php +++ b/src/Params/CookieInputParam.php @@ -8,12 +8,13 @@ class CookieInputParam extends InputParam { protected $type = self::TYPE_COOKIE; - public function getValue() + public function getValue(): mixed { if (!filter_has_var(INPUT_COOKIE, $this->key) && isset($_COOKIE[$this->key])) { return $_COOKIE[$this->key]; } + $value = filter_input(INPUT_COOKIE, $this->key); - return $value !== null ? $value : $this->default; + return $value ?? $this->default; } } diff --git a/src/Params/FileInputParam.php b/src/Params/FileInputParam.php index 11df2f8..53a3bdc 100644 --- a/src/Params/FileInputParam.php +++ b/src/Params/FileInputParam.php @@ -16,15 +16,20 @@ protected function addFormInput(Form $form, string $key): BaseControl return $form->addUpload($key, $this->getParamLabel()); } - public function getValue() + public function getValue(): mixed { if (isset($_FILES[$this->key])) { return $this->isMulti() ? $this->processMultiFileUploads($_FILES[$this->key]) : $_FILES[$this->key]; } + return $this->default; } - private function processMultiFileUploads($files) + /** + * @param array $files + * @return array + */ + private function processMultiFileUploads(array$files): array { $result = []; foreach ($files as $key => $values) { @@ -32,6 +37,7 @@ private function processMultiFileUploads($files) $result[$index][$key] = $value; } } + return $result; } } diff --git a/src/Params/GetInputParam.php b/src/Params/GetInputParam.php index b6b7a9c..9fe75ac 100644 --- a/src/Params/GetInputParam.php +++ b/src/Params/GetInputParam.php @@ -8,11 +8,12 @@ class GetInputParam extends InputParam { protected $type = self::TYPE_GET; - public function getValue() + public function getValue(): mixed { if (!filter_has_var(INPUT_GET, $this->key) && isset($_GET[$this->key])) { return $_GET[$this->key]; } + $value = $this->isMulti() ? filter_input(INPUT_GET, $this->key, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY) : filter_input(INPUT_GET, $this->key); return $value !== null && $value !== false ? $value : $this->default; } diff --git a/src/Params/InputParam.php b/src/Params/InputParam.php index 468520f..1244b71 100644 --- a/src/Params/InputParam.php +++ b/src/Params/InputParam.php @@ -12,16 +12,25 @@ abstract class InputParam implements ParamInterface { - const TYPE_POST = 'POST'; - const TYPE_GET = 'GET'; - const TYPE_PUT = 'PUT'; - const TYPE_FILE = 'FILE'; - const TYPE_COOKIE = 'COOKIE'; - const TYPE_POST_RAW = 'POST_RAW'; - const TYPE_POST_JSON = 'POST_JSON'; + public const TYPE_POST = 'POST'; - const OPTIONAL = false; - const REQUIRED = true; + public const TYPE_GET = 'GET'; + + public const TYPE_PUT = 'PUT'; + + public const TYPE_FILE = 'FILE'; + + public const TYPE_COOKIE = 'COOKIE'; + + public const TYPE_POST_RAW = 'POST_RAW'; + + public const TYPE_POST_JSON = 'POST_JSON'; + + public const OPTIONAL = false; + + public const REQUIRED = true; + + public const DEFAULT_MULTI_INPUT_COUNT = 5; /** @var string */ protected $type; @@ -32,7 +41,7 @@ abstract class InputParam implements ParamInterface /** @var bool */ protected $required = self::OPTIONAL; - /** @var array|null */ + /** @var array|null */ protected $availableValues = null; /** @var bool */ @@ -44,7 +53,7 @@ abstract class InputParam implements ParamInterface /** @var mixed */ protected $default; - /** @var array */ + /** @var array */ protected $examples = []; public function __construct(string $key) @@ -58,11 +67,15 @@ public function setRequired(): self return $this; } + /** + * @param array $availableValues + */ public function setAvailableValues(array $availableValues): self { if ($availableValues === array_values($availableValues)) { $availableValues = array_combine($availableValues, $availableValues); } + $this->availableValues = $availableValues; return $this; } @@ -88,6 +101,9 @@ public function isRequired(): bool return $this->required; } + /** + * @return array|null + */ public function getAvailableValues(): ?array { return $this->availableValues; @@ -111,7 +127,6 @@ public function getDescription(): string /** * @param mixed $default - * @return self */ public function setDefault($default): self { @@ -119,10 +134,7 @@ public function setDefault($default): self return $this; } - /** - * @return mixed - */ - public function getDefault() + public function getDefault(): mixed { return $this->default; } @@ -131,7 +143,6 @@ public function getDefault() * Add example, can be used multiple times to add many examples * @param string $name Example name * @param mixed $example Example - * @return Self */ public function addExample(string $name, $example): self { @@ -142,30 +153,26 @@ public function addExample(string $name, $example): self /** * Set default example * @param mixed $example - * @return self * @deprecated Use addExample instead */ public function setExample($example): self { - $this->examples["default"] = $example; + $this->examples['default'] = $example; return $this; } - /** - * Returns first example - * @return mixed - */ - public function getExample() + public function getExample(): mixed { if (empty($this->examples)) { return null; } + return reset($this->examples); } /** * Returns all examples - * @return array + * @return array */ public function getExamples(): array { @@ -174,22 +181,25 @@ public function getExamples(): array public function updateConsoleForm(Form $form): void { - $count = $this->isMulti() ? 5 : 1; // TODO moznost nastavit kolko inputov sa ma vygenerovat v konzole, default moze byt 5 + $count = $this->isMulti() ? self::DEFAULT_MULTI_INPUT_COUNT : 1; // TODO moznost nastavit kolko inputov sa ma vygenerovat v konzole, default moze byt 5 for ($i = 0; $i < $count; $i++) { $key = $this->getKey(); if ($this->isMulti()) { $key = $key . '___' . $i; } + $input = $this->addFormInput($form, $key); if ($this->description) { $input->setOption('description', Html::el('div', ['class' => 'param-description'])->setHtml($this->description)); } + if ($this->getExample() || $this->getDefault()) { $default = $this->getExample() ?: $this->getDefault(); $default = is_array($default) ? ($default[$i] ?? null) : $default; $input->setDefaultValue($default); } } + $form->addCheckbox('do_not_send_empty_value_for_' . $this->getKey(), 'Do not send empty value for ' . $this->getLabel()); } @@ -199,6 +209,7 @@ protected function addFormInput(Form $form, string $key): BaseControl return $form->addSelect($key, $this->getParamLabel(), $this->getAvailableValues()) ->setPrompt('Select ' . $this->getLabel()); } + return $form->addText($key, $this->getParamLabel()); } @@ -213,8 +224,7 @@ protected function getParamLabel(): string if ($this->isRequired()) { $title .= ' *'; } - $title .= ' (' . $this->getType() . ')'; - return $title; + return $title . (' (' . $this->getType() . ')'); } /** diff --git a/src/Params/JsonInputParam.php b/src/Params/JsonInputParam.php index f7ab440..05a71c9 100644 --- a/src/Params/JsonInputParam.php +++ b/src/Params/JsonInputParam.php @@ -15,9 +15,9 @@ class JsonInputParam extends InputParam { protected $type = self::TYPE_POST_JSON; - private $schema; + private string $schema; - private $rawInput; + private mixed $rawInput = null; public function __construct(string $key, string $schema) { @@ -30,12 +30,13 @@ public function setMulti(): InputParam throw new Exception('Cannot use multi json input param'); } - public function getValue() + public function getValue(): mixed { - $input = $this->rawInput = file_get_contents("php://input") ?: $this->default; + $input = $this->rawInput = file_get_contents('php://input') ?: $this->default; if ($input === null) { $input = ''; } + return json_decode($input, true); } @@ -54,7 +55,7 @@ public function validate(): ValidationResultInterface return new ValidationResult(ValidationResult::STATUS_OK); } - $value = json_decode(json_encode($value)); + $value = json_decode(json_encode($value) ?: ''); $schemaValidator = new JsonSchemaValidator(); return $schemaValidator->validate($value, $this->schema); } @@ -70,29 +71,29 @@ protected function addFormInput(Form $form, string $key): BaseControl if (!empty($examples = $this->getExamples())) { if (count($examples) === 1) { - $fullSchema['example'] = is_array($this->getExample())? $this->getExample() : json_decode($this->getExample(), true); + $fullSchema['example'] = is_array($this->getExample()) ? $this->getExample() : json_decode($this->getExample(), true); } else { foreach ($examples as $exampleKey => $example) { - $fullSchema['examples'][$exampleKey] = is_array($example)? $example : json_decode($example, true); + $fullSchema['examples'][$exampleKey] = is_array($example) ? $example : json_decode($example, true); // pretty formatting of json example if decoded } } } - if (!empty($fullSchema['examples'])) { $this->description .= <<< HTML
Select Example:  HTML; foreach ($fullSchema['examples'] as $exampleKey => $exampleValue) { - $example = htmlentities(json_encode($exampleValue, JSON_PRETTY_PRINT)); + $example = htmlentities(json_encode($exampleValue, JSON_PRETTY_PRINT) ?: ''); $this->description .= <<< HTML
{$exampleKey}
HTML; } + $this->description .= <<< HTML