diff --git a/src/Configuration.php b/src/Configuration.php index 07c6a61..e838854 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -3,6 +3,7 @@ namespace SunnyPHP\TTLock; +use SunnyPHP\TTLock\Contract\Request\RequiredConfiguration; use Webmozart\Assert\Assert; /** @@ -12,20 +13,27 @@ final class Configuration { private const ENDPOINT = 'https://euapi.ttlock.com'; private string $clientId; - private string $clientSecret; + private ?string $clientSecret; + private ?string $accessToken; private string $endpointHost; + private static array $toArrayFilter = [ + 'clientId' => RequiredConfiguration::CLIENT_ID, + 'clientSecret' => RequiredConfiguration::CLIENT_SECRET, + 'accessToken' => RequiredConfiguration::ACCESS_TOKEN, + ]; public function __construct( string $clientId, - string $clientSecret, + ?string $clientSecret = null, + ?string $accessToken = null, string $endpointHost = self::ENDPOINT ) { Assert::notEmpty($clientId, 'ClientId param should be filled'); - Assert::notEmpty($clientSecret, 'ClientSecret param should be filled'); Assert::notEmpty($endpointHost, 'EndpointHost param should be filled, use default value: ' . self::ENDPOINT); $this->clientId = $clientId; $this->clientSecret = $clientSecret; + $this->accessToken = $accessToken; $this->endpointHost = $endpointHost; } @@ -34,11 +42,16 @@ public function getClientId(): string return $this->clientId; } - public function getClientSecret(): string + public function getClientSecret(): ?string { return $this->clientSecret; } + public function getAccessToken(): ?string + { + return $this->accessToken; + } + public function getEndpointHost(): string { return $this->endpointHost; @@ -46,16 +59,36 @@ public function getEndpointHost(): string public function withClientId(string $clientId): self { - return new self($clientId, $this->getClientSecret(), $this->getEndpointHost()); + return new self($clientId, $this->getClientSecret(), $this->getAccessToken(), $this->getEndpointHost()); + } + + public function withClientSecret(?string $clientSecret): self + { + return new self($this->getClientId(), $clientSecret, $this->getAccessToken(), $this->getEndpointHost()); } - public function withClientSecret(string $clientSecret): self + public function withAccessToken(?string $accessToken): self { - return new self($this->getClientId(), $clientSecret, $this->getEndpointHost()); + return new self($this->getClientId(), $this->getClientSecret(), $accessToken, $this->getEndpointHost()); } public function withEndpointHost(string $endpointHost): self { - return new self($this->getClientId(), $this->getClientSecret(), $endpointHost); + return new self($this->getClientId(), $this->getClientSecret(), $this->getAccessToken(), $endpointHost); + } + + public function toArray(?int $bitmask = null): array + { + $params = []; + + foreach (self::$toArrayFilter as $key => $filterBit) { + if ($bitmask === null || ($bitmask & $filterBit) === $filterBit) { + Assert::propertyExists($this, $key); + + $params[$key] = $this->{$key}; + } + } + + return $params; } } diff --git a/src/Contract/Middleware/BeforeResponse/BeforeResponseInterface.php b/src/Contract/Middleware/BeforeResponse/BeforeResponseInterface.php new file mode 100644 index 0000000..209947d --- /dev/null +++ b/src/Contract/Middleware/BeforeResponse/BeforeResponseInterface.php @@ -0,0 +1,11 @@ + R\OAuth2\AccessToken::class, Request\OAuth2\RefreshAccessTokenInterface::class => R\OAuth2\RefreshAccessToken::class, Request\User\GetListInterface::class => R\User\GetList::class, Request\User\RegisterInterface::class => R\User\Register::class, + Request\User\ResetPasswordInterface::class => R\Common\SuccessResponse::class, + Request\User\DeleteInterface::class => R\Common\SuccessResponse::class, + Request\Lock\InitializeInterface::class => R\Lock\Initialize::class, ]; public function __construct( Configuration $configuration, - ?ClientInterface $httpClient = null, - ?RequestFactoryInterface $requestFactory = null, + ?Transport $transport = null, + array $middlewares = [], array $responseClasses = [] ) { $this->configuration = $configuration; - $this->transport = new Transport($configuration->getEndpointHost(), $httpClient, $requestFactory); + $this->transport = $transport ?: new Transport(); + $this->middleware = new Middleware($middlewares); if ($responseClasses !== []) { $this->setResponseClasses($responseClasses); } } + public function withConfiguration(Configuration $configuration): self + { + return new self($configuration, $this->transport, $this->responseClass); + } + + public function withConfigurationAccessToken(string $accessToken): self + { + return $this->withConfiguration($this->configuration->withAccessToken($accessToken)); + } + + public function addMiddleware(MiddlewareInterface $middleware): self + { + $this->middleware->add($middleware); + + return $this; + } + public function setResponseClasses(array $responseClasses): self { foreach ($responseClasses as $requestInterface => $responseClass) { @@ -66,24 +91,28 @@ public function setResponseClass(string $requestInterface, string $responseClass public function getResponse(Request\RequestInterface $request, string $responseClass): Response\ResponseInterface { + $url = $this->configuration->getEndpointHost() . $request->getEndpointUrl(); + $params = $request->toArray(); - if ($request->isClientCredentialsRequired()) { - $params = array_replace($params, [ - 'clientId' => $this->configuration->getClientId(), - 'clientSecret' => $this->configuration->getClientSecret(), - ]); + if ($bitmask = $request->getRequiredConfiguration()) { + $params = array_replace($params, $this->configuration->toArray($bitmask)); } if ($request->getEndpointMethod() === Request\Method::POST) { - $requestObject = $this->transport->createPostRequest($request->getEndpointUrl(), [ + $requestObject = $this->transport->createPostRequest($url, [ 'Content-Type' => 'application/x-www-form-urlencoded', ], http_build_query($params)); } else { - $requestObject = $this->transport->createGetRequest($request->getEndpointUrl(), $params); + $requestObject = $this->transport->createGetRequest($url, $params); } $responseArray = $this->transport->getEndpointResponse($requestObject); + foreach ($this->middleware->getAll(BeforeResponseInterface::class) as $middleware) { + /** @var BeforeResponseInterface $middleware */ + $responseArray = $middleware->handle($responseArray); + } + return new $responseClass($responseArray); } diff --git a/src/Exception/ApiException.php b/src/Exception/ApiException.php index 50e4ab0..6e7a48e 100644 --- a/src/Exception/ApiException.php +++ b/src/Exception/ApiException.php @@ -30,7 +30,7 @@ public function getResponse(): ?ResponseInterface public static function supports($content): bool { - return is_array($content) && isset($content['errmsg']); + return is_array($content) && isset($content['errcode']) && $content['errcode'] != 0; } public static function createFromArray(array $content, ?ResponseInterface $response = null): self diff --git a/src/Middleware.php b/src/Middleware.php new file mode 100644 index 0000000..1704140 --- /dev/null +++ b/src/Middleware.php @@ -0,0 +1,46 @@ +getType(); + Assert::notEmpty($type, 'Middleware type should be existed interface FQDN, like: ' . MiddlewareInterface::class); + Assert::classExists($type, 'Middleware is not existed interface FQDN: ' . $type); + + if (!array_key_exists($type, $this->collection)) { + $this->collection[$type] = []; + } + + if (!in_array($middleware, $this->collection[$type], true)) { + return $this; + } + + $this->collection[$type][] = $middleware; + + return $this; + } + + public function getAll(string $type): array + { + return $this->collection[$type] ?? []; + } +} diff --git a/src/Request/Lock/Initialize.php b/src/Request/Lock/Initialize.php new file mode 100644 index 0000000..da4f431 --- /dev/null +++ b/src/Request/Lock/Initialize.php @@ -0,0 +1,105 @@ +lockData = $lockData; + $this->lockAlias = $lockAlias; + $this->groupId = $groupId; + $this->nbInitSuccess = $nbInitSuccess; + $this->currentTime = $currentDate ?: new DateTimeImmutable(); + } + + public function getLockData(): string + { + return $this->lockData; + } + + public function getLockAlias(): ?string + { + return $this->lockAlias; + } + + public function getGroupId(): ?int + { + return $this->groupId; + } + + public function getNbInitSuccess(): ?bool + { + return $this->nbInitSuccess; + } + + public function getCurrentTime(): DateTimeImmutable + { + return $this->currentTime; + } + + public function getRequiredConfiguration(): int + { + return RequiredConfiguration::CLIENT_ID | RequiredConfiguration::ACCESS_TOKEN; + } + + public function getEndpointUrl(): string + { + return '/v3/lock/initialize'; + } + + public function getEndpointMethod(): string + { + return Method::POST; + } + + public function toArray(): array + { + $params = [ + 'lockData' => $this->getLockData(), + 'date' => $this->getCurrentTime()->getTimestamp() * 1000, + ]; + + if (($value = $this->getLockAlias()) !== null) { + $params['lockAlias'] = $value; + } + + if (($value = $this->getGroupId()) !== null) { + $params['groupId'] = $value; + } + + if (($value = $this->getNbInitSuccess()) !== null) { + $params['nbInitSuccess'] = (int) $value; + } + + return $params; + } +} diff --git a/src/Request/OAuth2/AccessToken.php b/src/Request/OAuth2/AccessToken.php index 8308b7c..cb280dd 100644 --- a/src/Request/OAuth2/AccessToken.php +++ b/src/Request/OAuth2/AccessToken.php @@ -5,6 +5,7 @@ use SunnyPHP\TTLock\Contract\Request\Method; use SunnyPHP\TTLock\Contract\Request\OAuth2\AccessTokenInterface; +use SunnyPHP\TTLock\Contract\Request\RequiredConfiguration; use Webmozart\Assert\Assert; final class AccessToken implements AccessTokenInterface @@ -42,9 +43,9 @@ public function getPassword(): string return $this->password; } - public function isClientCredentialsRequired(): bool + public function getRequiredConfiguration(): int { - return true; + return RequiredConfiguration::CLIENT_ID | RequiredConfiguration::CLIENT_SECRET; } public function getEndpointUrl(): string diff --git a/src/Request/OAuth2/RefreshAccessToken.php b/src/Request/OAuth2/RefreshAccessToken.php index 7f46485..7f22d05 100644 --- a/src/Request/OAuth2/RefreshAccessToken.php +++ b/src/Request/OAuth2/RefreshAccessToken.php @@ -5,6 +5,7 @@ use SunnyPHP\TTLock\Contract\Request\Method; use SunnyPHP\TTLock\Contract\Request\OAuth2\RefreshAccessTokenInterface; +use SunnyPHP\TTLock\Contract\Request\RequiredConfiguration; use Webmozart\Assert\Assert; final class RefreshAccessToken implements RefreshAccessTokenInterface @@ -37,9 +38,9 @@ public function getGrantType(): string return $this->grantType; } - public function isClientCredentialsRequired(): bool + public function getRequiredConfiguration(): int { - return true; + return RequiredConfiguration::CLIENT_ID | RequiredConfiguration::CLIENT_SECRET; } public function getEndpointUrl(): string diff --git a/src/Request/User/Delete.php b/src/Request/User/Delete.php new file mode 100644 index 0000000..5b4a31d --- /dev/null +++ b/src/Request/User/Delete.php @@ -0,0 +1,64 @@ +username = $username; + $this->currentTime = $currentDate ?: new DateTimeImmutable(); + } + + public function getUsername(): string + { + return $this->username; + } + + public function getCurrentTime(): DateTimeImmutable + { + return $this->currentTime; + } + + public function getRequiredConfiguration(): int + { + return RequiredConfiguration::CLIENT_ID | RequiredConfiguration::CLIENT_SECRET; + } + + public function getEndpointUrl(): string + { + return '/v3/user/delete'; + } + + public function getEndpointMethod(): string + { + return Method::POST; + } + + public function toArray(): array + { + return [ + 'username' => $this->getUsername(), + 'date' => $this->getCurrentTime()->getTimestamp() * 1000, + ]; + } +} diff --git a/src/Request/User/GetList.php b/src/Request/User/GetList.php index 36ba02a..c2a8fb1 100644 --- a/src/Request/User/GetList.php +++ b/src/Request/User/GetList.php @@ -4,6 +4,7 @@ namespace SunnyPHP\TTLock\Request\User; use SunnyPHP\TTLock\Contract\Request\Method; +use SunnyPHP\TTLock\Contract\Request\RequiredConfiguration; use SunnyPHP\TTLock\Contract\Request\User\GetListInterface; use Webmozart\Assert\Assert; @@ -64,9 +65,9 @@ public function getDate(): int return $this->date; } - public function isClientCredentialsRequired(): bool + public function getRequiredConfiguration(): int { - return true; + return RequiredConfiguration::CLIENT_ID | RequiredConfiguration::CLIENT_SECRET; } public function getEndpointUrl(): string diff --git a/src/Request/User/Register.php b/src/Request/User/Register.php index d4a570b..e9fda70 100644 --- a/src/Request/User/Register.php +++ b/src/Request/User/Register.php @@ -5,6 +5,7 @@ use DateTimeImmutable; use SunnyPHP\TTLock\Contract\Request\Method; +use SunnyPHP\TTLock\Contract\Request\RequiredConfiguration; use SunnyPHP\TTLock\Contract\Request\User\RegisterInterface; use Webmozart\Assert\Assert; @@ -52,9 +53,9 @@ public function getCurrentTime(): DateTimeImmutable return $this->currentTime; } - public function isClientCredentialsRequired(): bool + public function getRequiredConfiguration(): int { - return true; + return RequiredConfiguration::CLIENT_ID | RequiredConfiguration::CLIENT_SECRET; } public function getEndpointUrl(): string diff --git a/src/Request/User/ResetPassword.php b/src/Request/User/ResetPassword.php new file mode 100644 index 0000000..543eebd --- /dev/null +++ b/src/Request/User/ResetPassword.php @@ -0,0 +1,79 @@ +username = $username; + $this->password = $encryptedPassword ? $password : md5($password); + $this->currentTime = $currentDate ?: new DateTimeImmutable(); + } + + public function getUsername(): string + { + return $this->username; + } + + public function getPassword(): string + { + return $this->password; + } + + public function getCurrentTime(): DateTimeImmutable + { + return $this->currentTime; + } + + public function getRequiredConfiguration(): int + { + return RequiredConfiguration::CLIENT_ID | RequiredConfiguration::CLIENT_SECRET; + } + + public function getEndpointUrl(): string + { + return '/v3/user/resetPassword'; + } + + public function getEndpointMethod(): string + { + return Method::POST; + } + + public function toArray(): array + { + return [ + 'username' => $this->getUsername(), + 'password' => $this->getPassword(), + 'date' => $this->getCurrentTime()->getTimestamp() * 1000, + ]; + } +} diff --git a/src/Response/Common/SuccessResponse.php b/src/Response/Common/SuccessResponse.php new file mode 100644 index 0000000..c51ba0b --- /dev/null +++ b/src/Response/Common/SuccessResponse.php @@ -0,0 +1,15 @@ +validatePositiveInteger($response, 'lockId', 'keyId'); + + parent::__construct($response); + } + + public function getLockId(): int + { + return $this->response['lockId']; + } + + public function getKeyId(): int + { + return $this->response['keyId']; + } +} diff --git a/src/Transport.php b/src/Transport.php index 2787558..b0b4860 100644 --- a/src/Transport.php +++ b/src/Transport.php @@ -15,36 +15,29 @@ final class Transport { - private string $httpBase; private ClientInterface $httpClient; private RequestFactoryInterface $requestFactory; private StreamFactoryInterface $streamFactory; public function __construct( - string $httpBase, ?ClientInterface $httpClient = null, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null ) { - $this->httpBase = rtrim($httpBase, '/'); $this->httpClient = $httpClient ?: Psr18ClientDiscovery::find(); $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory(); $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(); } /** - * @see RequestFactoryInterface::createRequest() - * @param string $urlPartWithoutHost + * @param string $url * @param array $headers * @return RequestInterface + * @see RequestFactoryInterface::createRequest() */ - public function createPostRequest(string $urlPartWithoutHost, array $headers = [], ?string $body = null): RequestInterface + public function createPostRequest(string $url, array $headers = [], ?string $body = null): RequestInterface { - if (!str_starts_with($urlPartWithoutHost, '/')) { - $urlPartWithoutHost = '/' . $urlPartWithoutHost; - } - - $request = $this->requestFactory->createRequest('POST', $this->httpBase . $urlPartWithoutHost); + $request = $this->requestFactory->createRequest('POST', $url); foreach ($headers as $name => $values) { $request = $request->withHeader($name, $values); } @@ -56,16 +49,11 @@ public function createPostRequest(string $urlPartWithoutHost, array $headers = [ return $request; } - public function createGetRequest(string $urlPartWithoutHost, array $queryParams = [], array $headers = []): RequestInterface + public function createGetRequest(string $url, array $queryParams = [], array $headers = []): RequestInterface { - if (!str_starts_with($urlPartWithoutHost, '/')) { - $urlPartWithoutHost = '/' . $urlPartWithoutHost; - } - - $uri = str_contains($urlPartWithoutHost, '?') ? '&' : '?'; - $uri = $this->httpBase . $urlPartWithoutHost . ($queryParams ? $uri . http_build_query($queryParams) : ''); + $url .= ($queryParams ? (str_contains($url, '?') ? '&' : '?') . http_build_query($queryParams) : ''); - $request = $this->requestFactory->createRequest('GET', $uri); + $request = $this->requestFactory->createRequest('GET', $url); foreach ($headers as $name => $values) { $request = $request->withHeader($name, $values); } @@ -92,6 +80,7 @@ private function getResponseContentType(ResponseInterface $response): ?string /** * Returns API response (json decoded) or exception + * @param RequestInterface $request * @return array * @throws ApiException * @throws Throwable @@ -102,7 +91,13 @@ public function getEndpointResponse(RequestInterface $request): array try { $response = $this->httpClient->sendRequest($request); if (!($type = $this->getResponseContentType($response)) || !str_contains($type, 'application/json')) { - throw new ApiException('Unsupported response content type: ' . $type); + $message = 'Unsupported response content type: ' . $type; + if (str_contains($type, 'text/html')) { + $content = trim(preg_replace('~[\n\t]+|\s{2,}~', ' ', strip_tags($response->getBody()->getContents())), " .\t\n"); + $message .= ', content: ' . htmlspecialchars($content) . '.'; + } + + throw new ApiException($message); } $content = $response->getBody()->getContents();