From 7a157033f35ce10b2629ad3d4c0550f9dd913792 Mon Sep 17 00:00:00 2001 From: Daniel Seifert Date: Fri, 20 Dec 2024 23:45:55 +0100 Subject: [PATCH] add connection --- LICENSE | 21 +++ README.md | 45 ++++++ composer.json | 2 + src/Connection.php | 173 ++++++++++++++++++++++ src/Exceptions/BaseException.php | 14 ++ src/Exceptions/NoCredentialsException.php | 7 + src/Klicktipp.php | 74 +++++++++ src/Resources/Account.php | 51 +++++++ src/Resources/Model.php | 76 ++++++++++ 9 files changed, 463 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/Connection.php create mode 100644 src/Exceptions/BaseException.php create mode 100644 src/Exceptions/NoCredentialsException.php create mode 100644 src/Klicktipp.php create mode 100644 src/Resources/Account.php create mode 100644 src/Resources/Model.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2db6d45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 ProductFlow B.V. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..44ed92f --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +![stability-wip](https://img.shields.io/badge/stability-work_in_progress-lightgrey.svg) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/d3datadevelopment/klicktipp-php-client?include_prereleases) [![MIT License](https://img.shields.io/github/license/d3datadevelopment/klicktipp-php-client)](https://git.d3data.de/D3Private/klicktipp-php-client/raw/branch/main/LICENSE) + +# klicktipp-php-client + +An unofficial client for the Klicktipp API. + +## Installation +This project can easily be installed through Composer. + +``` +composer require d3/klicktipp-php-client +``` + +## Set-up connection +Prepare the client for connecting to Klicktipp with your client key and secret key. + +```php +$klicktipp = new \D3\KlicktippPhpClient\Klicktipp( + $clientkey, + $secretkey, + new \GuzzleHttp\Client(...) // optional +); +``` + +## search a subscriber + +```php +$subscriberId = $klicktipp->subscriber()->search('me@johndoe.net'); +$subscriber = $klicktipp->subscriber()->search($subscriberId); +``` + +## Supported endpoints (still being added) + +[API](https://www.klicktipp.com/de/support/wissensdatenbank/rest-application-programming-interface-api/) + +:white_check_mark: = Done, and tested
+:ballot_box_with_check: = Done, but not yet tested
+:x: = Not yet developed
+:heavy_exclamation_mark: = deprecated/not supported
+ +| Endpoint | Status | +|--------------------------------------------------------------------------------------|-------------------------| +| account | :ballot_box_with_check: | +| subscriber | :ballot_box_with_check: | +| tag | :white_check_mark: | diff --git a/composer.json b/composer.json index 1e3d347..0f3f30d 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,8 @@ ], "require": { "php": ">=8.0", + "composer/composer": "^2.7.1", + "doctrine/collections": "^1.8.0", "guzzlehttp/guzzle": "~7.0", "ext-json": "*" }, diff --git a/src/Connection.php b/src/Connection.php new file mode 100644 index 0000000..450e66b --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,173 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\KlicktippPhpClient; + +use Composer\InstalledVersions; +use D3\KlicktippPhpClient\Exceptions\BaseException; +use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Exception\RequestException; +use Psr\Http\Message\ResponseInterface; +use RuntimeException; + +class Connection +{ + public const URL = 'https://api.klicktipp.com/'; + + public const USERAGENT = 'Klicktipp-php-client'; + + protected string $client_key; + + protected string $secret_key; + + protected ?string $cookie = null; + + /** + * Contains the HTTP client (e.g. Guzzle) + */ + private ClientInterface $client; + + public function __construct(string $client_key, string $secret_key) + { + $this->client_key = $client_key; + $this->secret_key = $secret_key; + } + + public function getClientKey(): string + { + return $this->client_key; + } + + public function getSecretKey(): string + { + return $this->secret_key; + } + + /** + * @param ClientInterface $client + */ + public function setClient(ClientInterface $client): void + { + $this->client = $client; + } + + /** + * @return ClientInterface + */ + public function getClient(): ClientInterface + { + $this->client = $this->client ?? + new Client([ + 'base_uri' => self::URL, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'User-Agent' => self::USERAGENT.'/'.InstalledVersions::getVersion('d3/klicktipp-php-client') + ] + ]); + + return $this->client; + } + + /** + * @param string $method + * @param string $uri + * @param array $options + * @return ResponseInterface + * @throws BaseException|GuzzleException + */ + public function request(string $method, string $uri, array $options = []): ResponseInterface + { + try { + $options['query'] = $options['query'] ?? []; + + if (! empty($options['body'])) { + $options['body'] = json_encode($options['body']); + } + + $header = [ + 'Cookie' => $this->cookie ?? '' + ]; + $options['headers'] = $header; + + return $this->getClient()->request($method, $uri, $options); + } catch (RequestException $e) { + if ($e->hasResponse()) { + $this->parseResponse($e->getResponse()); + } + + throw new BaseException( + $e->getResponse()->getBody(), + $e->getResponse()->getStatusCode(), + $e + ); + } + } + + /** + * @param string $method + * @param string $uri + * @param array $options + * @return array + * @throws BaseException + * @throws GuzzleException + */ + public function requestAndParse(string $method, string $uri, array $options = []): array + { + return $this->parseResponse($this->request($method, $uri, $options)); + } + + /** + * @param ResponseInterface $response + * @return array Parsed JSON result + * @throws BaseException + */ + public function parseResponse(ResponseInterface $response): array + { + try { + // Rewind the response (middlewares might have read it already) + $response->getBody()->rewind(); + + $response_body = $response->getBody()->getContents(); + $result_array = json_decode($response_body, true); + + if ($response->getStatusCode() === 204) { + return []; + } + + if (! is_array($result_array)) { + throw new BaseException( + sprintf('%s: %s', $response->getStatusCode(), $response_body), + $response->getStatusCode() + ); + } + + return $result_array; + } catch (RuntimeException $e) { + throw new BaseException( + $e->getMessage(), + 0, + $e + ); + } + } + + public function setCookie(?string $cookie): void + { + $this->cookie = $cookie; + } +} diff --git a/src/Exceptions/BaseException.php b/src/Exceptions/BaseException.php new file mode 100644 index 0000000..c249cec --- /dev/null +++ b/src/Exceptions/BaseException.php @@ -0,0 +1,14 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\KlicktippPhpClient; + +use D3\KlicktippPhpClient\Exceptions\BaseException; +use D3\KlicktippPhpClient\Resources\Account; +use D3\KlicktippPhpClient\Resources\Subscriber; +use D3\KlicktippPhpClient\Resources\Tag; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\GuzzleException; + +class Klicktipp +{ + protected string $client_key; + + protected string $secret_key; + + protected ?Connection $connection = null; + + /** + * @throws BaseException + * @throws GuzzleException + */ + public function __construct( + string $client_key, + string $secret_key, + ClientInterface $client = null + ){ + $this->client_key = $client_key; + $this->secret_key = $secret_key; + + if ($client) { + $this->getConnection()->setClient($client); + } + + $this->account()->login(); + } + + private function getConnection(): Connection + { + if (!$this->connection) { + $this->connection = new Connection($this->client_key, $this->secret_key); + } + return $this->connection; + } + + public function account(): Account + { + return new Account($this->getConnection()); + } + + public function subscriber(): Subscriber + { + return new Subscriber($this->getConnection()); + } + + public function tag(): Tag + { + return new Tag($this->getConnection()); + } +} diff --git a/src/Resources/Account.php b/src/Resources/Account.php new file mode 100644 index 0000000..1317f2f --- /dev/null +++ b/src/Resources/Account.php @@ -0,0 +1,51 @@ +connection->request( + 'POST', + 'account/login', + [ + 'query' => $this->getQuery(), + 'form_params' => [ + 'username' => $this->connection->getClientKey(), + 'password' => $this->connection->getSecretKey() + ] + ] + ); + + $this->connection->setCookie( + current($response->getHeader('set-cookie')) + ); + + return $this->connection->parseResponse($response); + } + + /** + * @throws BaseException|GuzzleException + */ + public function logout(): bool + { + $response = $this->connection->requestAndParse( + 'POST', + 'account/logout', + ['query' => $this->getQuery()] + ); + + if (current($response)) { + $this->connection->setCookie(null); + } + + return (bool) current($response); + } +} diff --git a/src/Resources/Model.php b/src/Resources/Model.php new file mode 100644 index 0000000..4f54016 --- /dev/null +++ b/src/Resources/Model.php @@ -0,0 +1,76 @@ +connection = $connection; + } + + public function getLimit(): ?int + { + return $this->limit; + } + + public function setLimit(int $limit): static + { + $this->limit = $limit; + return $this; + } + + public function getOffset(): ?int + { + return $this->offset; + } + + public function setOffset(int $offset): static + { + $this->offset = $offset; + return $this; + } + + public function setFilter(string $column, $operation, $value = null): static + { + if (is_null($value)) { + $value = $operation; + $operation = 'eq'; + } + + $this->filters[$column][$operation] = $value; + return $this; + } + + public function getQuery(): array + { + return array_filter(array_merge( + $this->query, + $this->filters, + [ 'limit' => $this->getLimit(), 'offset' => $this->getOffset() ] + )); + } + + public function setQuery(string|array $key, mixed $value = null): static + { + if (is_array($key)) { + $this->query = $key; + } else { + $this->query[$key] = $value; + } + + return $this; + } +}