diff --git a/README.md b/README.md index 5195a2c..dbbdc76 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,18 @@ [![Total Downloads](https://img.shields.io/packagist/dt/anikeen/id.svg?style=flat-square)](https://packagist.org/packages/anikeen/id) [![License](https://img.shields.io/packagist/l/anikeen/id.svg?style=flat-square)](https://packagist.org/packages/anikeen/id) -PHP Anikeen ID API Client for Laravel 10+ +PHP Anikeen ID API Client for Laravel 11+ ## Table of contents 1. [Installation](#installation) 2. [Event Listener](#event-listener) 3. [Configuration](#configuration) -4. [Examples](#examples) -5. [Documentation](#documentation) -6. [Development](#Development) +4. [Implementing Auth](#implementing-auth) +5. [General](#general) +6. [Examples](#examples) +7. [Documentation](#documentation) +8. [Development](#Development) ## Installation @@ -23,39 +25,22 @@ composer require anikeen/id ## Event Listener -- Add `SocialiteProviders\Manager\SocialiteWasCalled` event to your `listen[]` array in `app/Providers/EventServiceProvider`. -- Add your listeners (i.e. the ones from the providers) to the `SocialiteProviders\Manager\SocialiteWasCalled[]` that you just created. -- The listener that you add for this provider is `'Anikeen\\Id\\Socialite\\AnikeenIdExtendSocialite@handle',`. -- Note: You do not need to add anything for the built-in socialite providers unless you override them with your own providers. +In Laravel 11, the default EventServiceProvider provider was removed. Instead, add the listener using the listen method on the Event facade, in your `AppServiceProvider` ``` -/** - * The event handler mappings for the application. - * - * @var array - */ -protected $listen = [ - \SocialiteProviders\Manager\SocialiteWasCalled::class => [ - // add your listeners (aka providers) here - 'Anikeen\\Id\\Socialite\\AnikeenIdExtendSocialite@handle', - ], -]; +Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { + $event->extendSocialite('anikeen-id', \Anikeen\Id\Socialite\Provider::class); +}); ``` ## Configuration -Copy configuration to config folder: - -``` -$ php artisan vendor:publish --provider="Anikeen\Id\Providers\AnikeenIdServiceProvider" -``` - Add environmental variables to your `.env` ``` ANIKEEN_ID_KEY= ANIKEEN_ID_SECRET= -ANIKEEN_ID_REDIRECT_URI=http://localhost +ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback ``` You will need to add an entry to the services configuration file so that after config files are cached for usage in production environment (Laravel command `artisan config:cache`) all config is still available. @@ -63,13 +48,20 @@ You will need to add an entry to the services configuration file so that after c **Add to `config/services.php`:** ```php -'anikeen-id' => [ +'anikeen' => [ 'client_id' => env('ANIKEEN_ID_KEY'), 'client_secret' => env('ANIKEEN_ID_SECRET'), - 'redirect' => env('ANIKEEN_ID_REDIRECT_URI') + 'redirect' => env('ANIKEEN_ID_CALLBACK_URL'), + 'base_url' => env('ANIKEEN_ID_BASE_URL'), ], ``` +```php +$middleware->web(append: [ + \Anikeen\Id\Http\Middleware\CreateFreshApiToken::class, +]); +``` + ## Implementing Auth This method should typically be called in the `boot` method of your `AuthServiceProvider` class: @@ -132,30 +124,9 @@ reference the provider in the `providers` configuration of your `auth.php` confi ], ``` -## Examples +## General -#### Basic - -```php -$anikeenId = new Anikeen\IdAnikeenId(); - -$anikeenId->setClientId('abc123'); - -// Get SSH Key by User ID -$result = $anikeenId->getSshKeysByUserId(38); - -// Check, if the query was successfull -if ( ! $result->success()) { - die('Ooops: ' . $result->error()); -} - -// Shift result to get single key data -$sshKey = $result->shift(); - -echo $sshKey->name; -``` - -#### Setters +#### Setters and Getters ```php $anikeenId = new Anikeen\Id\AnikeenId(); @@ -169,6 +140,72 @@ $anikeenId = $anikeenId->withClientSecret('abc123'); $anikeenId = $anikeenId->withToken('abcdef123456'); ``` +#### Error handling for an unsuccessful query: + +```php +$result = $anikeenId->sshKeysByUserId('someInvalidId'); + +// Check, if the query was successfully +if (!$result->success()) { + die('Ooops: ' . $result->error()); +} +``` + +#### Shift result to get single key data: + +```php +$result = $anikeenId->sshKeysByUserId('someValidId'); + +$sshKey = $result->shift(); + +echo $sshKey->name; +``` + +## Examples + +#### Get User SSH Key + +```php +$anikeenId = new Anikeen\IdAnikeenId(); + +$anikeenId->setClientId('abc123'); + +// Get SSH Key by User ID +$result = $anikeenId->sshKeysByUserId('someValidId'); + +// Check, if the query was successfully +if (!$result->success()) { + die('Ooops: ' . $result->error()); +} + +// Shift result to get single key data +$sshKey = $result->shift(); + +echo $sshKey->name; +``` + +#### Create Order Preview + +```php +$anikeenId = new \Anikeen\Id\AnikeenId(); + +// Create new Order Preview +$result = $anikeenId->createOrderPreview([ + 'country_iso' => 'de', + 'items' => [ + [ + 'type' => 'physical', + 'name' => 'Test', + 'price' => 2.99, + 'unit' => 'onetime', + 'units' => 1, + ] + ] +])->shift(); + +echo $preview->gross_total; +``` + #### OAuth Tokens ```php @@ -202,52 +239,109 @@ AnikeenId::withClientId('abc123')->withToken('abcdef123456')->getAuthedUser(); ## Documentation +## AnikeenId + ### Oauth ```php -public function retrievingToken(string $grantType, array $attributes) +public function retrievingToken(string $grantType, array $attributes): Result ``` -### SshKeys +### ManagesPricing ```php -public function getSshKeysByUserId(int $id) -public function createSshKey(string $publicKey, string $name = NULL) -public function deleteSshKey(int $id) +public function createOrderPreview(array $attributes = []): Result ``` -### Users +### ManagesSshKeys ```php -public function getAuthedUser() -public function createUser(array $parameters) -public function isEmailExisting(string $email) +public function sshKeysByUserId(string $sskKeyId): Result +public function createSshKey(string $publicKey, ?string $name = null): Result +public function deleteSshKey(int $sshKeyId): Result ``` -### Delete +### ManagesUsers ```php - +public function getAuthedUser(): Result +public function createUser(array $attributes): Result +public function isEmailExisting(string $email): Result ``` -### Get + +## Billable + +### ManagesBalance ```php - +public function balance(): float +public function charges(): float +public function charge(float $amount, string $paymentMethodId, array $options = []): Result ``` -### Post +### ManagesInvoices ```php - +public function invoices(): Result +public function invoice(string $invoiceId): Result +public function getInvoiceDownloadUrl(string $invoiceId): string ``` -### Put +### ManagesOrders ```php - +public function orders(): Result +public function createOrder(array $attributes = []): Result +public function order(string $orderId): Result +public function updateOrder(string $orderId, array $attributes = []): Result +public function checkoutOrder(string $orderId): Result +public function revokeOrder(string $orderId): Result +public function deleteOrder(string $orderId): Result +public function orderItems(string $orderId): Result +public function createOrderItem(string $orderId, array $attributes = []): Result +public function orderItem(string $orderId, string $orderItemId): Result +public function updateOrderItem(string $orderId, string $orderItemId, array $attributes = []): Result +public function deleteOrderItem(string $orderId, string $orderItemId): Result ``` +### ManagesPaymentMethods + +```php +public function hasPaymentMethod(): bool +public function paymentMethods(): Result +public function hasDefaultPaymentMethod(): bool +public function defaultPaymentMethod(): Result +public function billingPortalUrl(string $returnUrl, array $options): string +public function createSetupIntent(array $options = []): Result +``` + +### ManagesSubscriptions + +```php +public function subscriptions(): Result +public function subscription(string $subscriptionId): Result +public function createSubscription(array $attributes): Result +public function checkoutSubscription(string $subscriptionId): Result +public function revokeSubscription(string $subscriptionId): Result +public function resumeSubscription(string $subscriptionId): Result +``` + +### ManagesTaxation + +```php +public function vat(): float +``` + +### ManagesTransactions + +```php +public function transactions(): Result +public function createTransaction(array $attributes = []): Result +public function transaction(string $transactionId): Result +``` + + [**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Enums/Scope.php) ## Development diff --git a/README.stub b/README.stub index 757784b..08a5aee 100644 --- a/README.stub +++ b/README.stub @@ -11,9 +11,11 @@ PHP Anikeen ID API Client for Laravel 11+ 1. [Installation](#installation) 2. [Event Listener](#event-listener) 3. [Configuration](#configuration) -4. [Examples](#examples) -5. [Documentation](#documentation) -6. [Development](#Development) +4. [Implementing Auth](#implementing-auth) +5. [General](#general) +6. [Examples](#examples) +7. [Documentation](#documentation) +8. [Development](#Development) ## Installation @@ -33,18 +35,12 @@ Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { ## Configuration -Copy configuration to config folder: - -``` -$ php artisan vendor:publish --provider="Anikeen\Id\Providers\AnikeenIdServiceProvider" -``` - Add environmental variables to your `.env` ``` ANIKEEN_ID_KEY= ANIKEEN_ID_SECRET= -ANIKEEN_ID_REDIRECT_URI=http://localhost +ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback ``` You will need to add an entry to the services configuration file so that after config files are cached for usage in production environment (Laravel command `artisan config:cache`) all config is still available. @@ -52,13 +48,20 @@ You will need to add an entry to the services configuration file so that after c **Add to `config/services.php`:** ```php -'anikeen-id' => [ +'anikeen' => [ 'client_id' => env('ANIKEEN_ID_KEY'), 'client_secret' => env('ANIKEEN_ID_SECRET'), - 'redirect' => env('ANIKEEN_ID_REDIRECT_URI') + 'redirect' => env('ANIKEEN_ID_CALLBACK_URL'), + 'base_url' => env('ANIKEEN_ID_BASE_URL'), ], ``` +```php +$middleware->web(append: [ + \Anikeen\Id\Http\Middleware\CreateFreshApiToken::class, +]); +``` + ## Implementing Auth This method should typically be called in the `boot` method of your `AuthServiceProvider` class: @@ -121,30 +124,9 @@ reference the provider in the `providers` configuration of your `auth.php` confi ], ``` -## Examples +## General -#### Basic - -```php -$anikeenId = new Anikeen\IdAnikeenId(); - -$anikeenId->setClientId('abc123'); - -// Get SSH Key by User ID -$result = $anikeenId->getSshKeysByUserId(38); - -// Check, if the query was successfull -if ( ! $result->success()) { - die('Ooops: ' . $result->error()); -} - -// Shift result to get single key data -$sshKey = $result->shift(); - -echo $sshKey->name; -``` - -#### Setters +#### Setters and Getters ```php $anikeenId = new Anikeen\Id\AnikeenId(); @@ -158,6 +140,72 @@ $anikeenId = $anikeenId->withClientSecret('abc123'); $anikeenId = $anikeenId->withToken('abcdef123456'); ``` +#### Error handling for an unsuccessful query: + +```php +$result = $anikeenId->sshKeysByUserId('someInvalidId'); + +// Check, if the query was successfully +if (!$result->success()) { + die('Ooops: ' . $result->error()); +} +``` + +#### Shift result to get single key data: + +```php +$result = $anikeenId->sshKeysByUserId('someValidId'); + +$sshKey = $result->shift(); + +echo $sshKey->name; +``` + +## Examples + +#### Get User SSH Key + +```php +$anikeenId = new Anikeen\IdAnikeenId(); + +$anikeenId->setClientId('abc123'); + +// Get SSH Key by User ID +$result = $anikeenId->sshKeysByUserId('someValidId'); + +// Check, if the query was successfully +if (!$result->success()) { + die('Ooops: ' . $result->error()); +} + +// Shift result to get single key data +$sshKey = $result->shift(); + +echo $sshKey->name; +``` + +#### Create Order Preview + +```php +$anikeenId = new \Anikeen\Id\AnikeenId(); + +// Create new Order Preview +$result = $anikeenId->createOrderPreview([ + 'country_iso' => 'de', + 'items' => [ + [ + 'type' => 'physical', + 'name' => 'Test', + 'price' => 2.99, + 'unit' => 'onetime', + 'units' => 1, + ] + ] +])->shift(); + +echo $preview->gross_total; +``` + #### OAuth Tokens ```php diff --git a/config/anikeen-id.php b/config/anikeen-id.php deleted file mode 100644 index 0335805..0000000 --- a/config/anikeen-id.php +++ /dev/null @@ -1,8 +0,0 @@ - env('ANIKEEN_ID_KEY'), - 'client_secret' => env('ANIKEEN_ID_SECRET'), - 'redirect_url' => env('ANIKEEN_ID_REDIRECT_URI'), - 'base_url' => env('ANIKEEN_ID_BASE_URL'), -]; \ No newline at end of file diff --git a/generator/generate-docs.php b/generator/generate-docs.php index 68d1fbe..84e01e2 100644 --- a/generator/generate-docs.php +++ b/generator/generate-docs.php @@ -1,85 +1,111 @@ -map(function ($trait) { - - $title = str_replace('Trait', '', Arr::last(explode('\\', $trait))); - - $methods = []; - - $reflection = new ReflectionClass($trait); - - collect($reflection->getMethods()) - ->reject(function (ReflectionMethod $method) { - return $method->isAbstract(); - }) - ->reject(function (ReflectionMethod $method) { - return $method->isPrivate() || $method->isProtected(); - }) - ->reject(function (ReflectionMethod $method) { - return $method->isConstructor(); - }) - ->each(function (ReflectionMethod $method) use (&$methods, $title, $trait) { - - $declaration = collect($method->getModifiers())->map(function (int $modifier) { - return $modifier == ReflectionMethod::IS_PUBLIC ? 'public ' : ''; - })->join(' '); - - $declaration .= 'function '; - $declaration .= $method->getName(); - $declaration .= '('; - - $declaration .= collect($method->getParameters())->map(function (ReflectionParameter $parameter) { - - $parameterString = Arr::last(explode('\\', $parameter->getType()->getName())); - $parameterString .= ' '; - $parameterString .= '$'; - $parameterString .= $parameter->getName(); - - if ($parameter->isDefaultValueAvailable()) { - $parameterString .= ' = '; - $parameterString .= str_replace(PHP_EOL, '', var_export($parameter->getDefaultValue(), true)); - } - - return $parameterString; - - })->join(', '); - - $declaration .= ')'; - - $methods[] = $declaration; - }); - - return [$title, $methods]; - }) - ->map(function ($args) { - - list($title, $methods) = $args; - - $markdown = '### ' . $title; - $markdown .= PHP_EOL . PHP_EOL; - $markdown .= '```php'; - $markdown .= PHP_EOL; - - $markdown .= collect($methods)->each(function ($method) { - return $method; - })->implode(PHP_EOL); - - $markdown .= PHP_EOL; - $markdown .= '```'; - - return $markdown; - })->join(PHP_EOL . PHP_EOL); - -$markdown = str_replace("array (\n)", '[]', $markdown); - -$content = file_get_contents(__DIR__ . '/../README.stub'); - -$content = str_replace('', $markdown, $content); - -file_put_contents(__DIR__ . '/../README.md', $content); \ No newline at end of file +map(function (string $class) { + $className = Arr::last(explode('\\', $class)); + $markdown = "## {$className}\n\n"; + + // alle Traits der Klasse, außer denen aus ApiOperations + $traits = collect(class_uses($class) ?: []) + ->reject(function (string $trait) { + return Str::contains($trait, 'ApiOperations\\'); + }) + ->all(); + + if (empty($traits)) { + $markdown .= '_Keine Traits gefunden._'; + return $markdown; + } + + // für jeden Trait die Methoden extrahieren + $markdown .= collect($traits) + ->map(function (string $trait) { + $title = str_replace('Trait', '', Arr::last(explode('\\', $trait))); + $reflection = new ReflectionClass($trait); + + $methods = collect($reflection->getMethods()) + ->reject->isAbstract() + ->reject->isPrivate() + ->reject->isProtected() + ->reject->isConstructor() + ->map(function (ReflectionMethod $method) { + // Methodendeklaration starten + $decl = 'public function ' . $method->getName() . '('; + + // Parameter-Typen und Default-Werte + $decl .= collect($method->getParameters()) + ->map(function (ReflectionParameter $p) { + // Typ-Hint + $typeHint = ''; + if ($p->hasType()) { + $type = $p->getType(); + $nullable = $type->allowsNull() ? '?' : ''; + $name = Arr::last(explode('\\', $type->getName())); + $typeHint = $nullable . $name . ' '; + } + + // Parameter-Name + $param = $typeHint . '$' . $p->getName(); + + // Default-Wert + if ($p->isDefaultValueAvailable()) { + $default = $p->getDefaultValue(); + if (is_array($default) && empty($default)) { + // leeres Array → Short-Syntax + $param .= ' = []'; + } elseif ($default === null) { + // NULL → null (kleingeschrieben) + $param .= ' = null'; + } else { + // sonst var_export, Newlines entfernen + $def = var_export($default, true); + $param .= ' = ' . str_replace(PHP_EOL, '', $def); + } + } + + return $param; + }) + ->implode(', '); + + $decl .= ')'; + + // Rückgabetyp, falls vorhanden + if ($method->hasReturnType()) { + $retType = $method->getReturnType(); + $nullable = $retType->allowsNull() ? '?' : ''; + $typeName = Arr::last(explode('\\', $retType->getName())); + $decl .= ': ' . $nullable . $typeName; + } + + return $decl; + }) + ->all(); + + // Markdown-Block für diesen Trait + $md = "### {$title}\n\n```php\n"; + $md .= implode("\n", $methods) . "\n```\n"; + return $md; + }) + ->implode("\n"); + + return $markdown; + }) + ->implode("\n\n"); + +// README zusammenbauen und schreiben +$stub = file_get_contents(__DIR__ . '/../README.stub'); +$content = str_replace('', $allMarkdown, $stub); +file_put_contents(__DIR__ . '/../README.md', $content); diff --git a/phpunit.xml b/phpunit.xml index fcdfeaa..eff0718 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,7 +23,7 @@ - + \ No newline at end of file diff --git a/src/Id/AnikeenId.php b/src/Id/AnikeenId.php index 9bc7870..b995bca 100644 --- a/src/Id/AnikeenId.php +++ b/src/Id/AnikeenId.php @@ -2,6 +2,9 @@ namespace Anikeen\Id; +use Anikeen\Id\Concerns\ManagesPricing; +use Anikeen\Id\Concerns\ManagesSshKeys; +use Anikeen\Id\Concerns\ManagesUsers; use Anikeen\Id\Exceptions\RequestRequiresAuthenticationException; use Anikeen\Id\Exceptions\RequestRequiresClientIdException; use Anikeen\Id\Exceptions\RequestRequiresRedirectUriException; @@ -15,14 +18,16 @@ use Illuminate\Contracts\Auth\Authenticatable; class AnikeenId { - use Traits\OauthTrait; - use Traits\SshKeysTrait; - use Traits\UsersTrait; + use OauthTrait; + use ManagesPricing; + use ManagesSshKeys; + use ManagesUsers; use ApiOperations\Delete; use ApiOperations\Get; use ApiOperations\Post; use ApiOperations\Put; + use ApiOperations\Request; /** * The name for API token cookies. @@ -41,8 +46,21 @@ class AnikeenId */ public static bool $unserializesCookies = false; + /** + * The base URL for Anikeen ID API. + */ private static string $baseUrl = 'https://id.anikeen.com/api/'; + /** + * The key for the access token. + */ + private static string $accessTokenKey = 'anikeen_id_token'; + + /** + * The key for the access token. + */ + private static string $refreshTokenKey = 'anikeen_id_refresh_token'; + /** * Guzzle is used to make http requests. */ @@ -55,13 +73,11 @@ class AnikeenId /** * Anikeen ID OAuth token. - * */ protected ?string $token = null; /** * Anikeen ID client id. - * */ protected ?string $clientId = null; @@ -80,17 +96,17 @@ class AnikeenId */ public function __construct() { - if ($clientId = config('anikeen_id.client_id')) { + if ($clientId = config('services.anikeen.client_id')) { $this->setClientId($clientId); } - if ($clientSecret = config('anikeen_id.client_secret')) { + if ($clientSecret = config('services.anikeen.client_secret')) { $this->setClientSecret($clientSecret); } - if ($redirectUri = config('anikeen_id.redirect_url')) { + if ($redirectUri = config('services.anikeen.redirect')) { $this->setRedirectUri($redirectUri); } - if ($redirectUri = config('anikeen_id.base_url')) { - self::setBaseUrl($redirectUri); + if ($baseUrl = config('services.anikeen.base_url')) { + self::setBaseUrl($baseUrl); } $this->client = new Client([ 'base_uri' => self::$baseUrl, @@ -107,13 +123,33 @@ class AnikeenId self::$baseUrl = $baseUrl; } + public static function useAccessTokenKey(string $accessTokenKey): void + { + self::$accessTokenKey = $accessTokenKey; + } + + public static function getAccessTokenKey(): string + { + return self::$accessTokenKey; + } + + public static function useRefreshTokenKey(string $refreshTokenKey): void + { + self::$refreshTokenKey = $refreshTokenKey; + } + + public static function getRefreshTokenKey(): string + { + return self::$refreshTokenKey; + } + /** * Get or set the name for API token cookies. * * @param string|null $cookie * @return string|static */ - public static function cookie(string $cookie = null) + public static function cookie(string $cookie = null): string|static { if (is_null($cookie)) { return static::$cookie; @@ -127,7 +163,7 @@ class AnikeenId /** * Set the current user for the application with the given scopes. */ - public static function actingAs(Authenticatable|Traits\HasAnikeenTokens $user, array $scopes = [], string $guard = 'api'): Authenticatable + public static function actingAs(Authenticatable|HasAnikeenTokens $user, array $scopes = [], string $guard = 'api'): Authenticatable { $user->withAnikeenAccessToken((object)[ 'scopes' => $scopes @@ -251,12 +287,25 @@ class AnikeenId } /** - * @throws GuzzleException + * Get client id. + * * @throws RequestRequiresClientIdException */ - public function get(string $path = '', array $parameters = [], Paginator $paginator = null): Result + public function getClientId(): string { - return $this->query('GET', $path, $parameters, $paginator); + if (!$this->clientId) { + throw new RequestRequiresClientIdException; + } + + return $this->clientId; + } + + /** + * Set client id. + */ + public function setClientId(string $clientId): void + { + $this->clientId = $clientId; } /** @@ -265,23 +314,23 @@ class AnikeenId * @throws GuzzleException * @throws RequestRequiresClientIdException */ - public function query(string $method = 'GET', string $path = '', array $parameters = [], Paginator $paginator = null, mixed $jsonBody = null): Result + public function request(string $method, string $path, null|array $payload = null, array $parameters = [], Paginator $paginator = null): Result { - /** @noinspection DuplicatedCode */ if ($paginator !== null) { $parameters[$paginator->action] = $paginator->cursor(); } + try { $response = $this->client->request($method, $path, [ - 'headers' => $this->buildHeaders((bool)$jsonBody), + 'headers' => $this->buildHeaders((bool)$payload), 'query' => Query::build($parameters), - 'json' => $jsonBody ?: null, + 'json' => $payload ?: null, ]); - $result = new Result($response, null, $paginator); + + $result = new Result($response, null, $this); } catch (RequestException $exception) { - $result = new Result($exception->getResponse(), $exception, $paginator); + $result = new Result($exception->getResponse(), $exception, $this); } - $result->anikeenId = $this; return $result; } @@ -308,64 +357,38 @@ class AnikeenId } /** - * Get client id. - * + * @throws GuzzleException * @throws RequestRequiresClientIdException */ - public function getClientId(): string + public function get(string $path, array $parameters = [], Paginator $paginator = null): Result { - if (!$this->clientId) { - throw new RequestRequiresClientIdException; - } - - return $this->clientId; - } - - /** - * Set client id. - */ - public function setClientId(string $clientId): void - { - $this->clientId = $clientId; + return $this->request('GET', $path, null, $parameters, $paginator); } /** * @throws GuzzleException * @throws RequestRequiresClientIdException */ - public function post(string $path = '', array $parameters = [], Paginator $paginator = null): Result + public function post(string $path, array $payload = [], array $parameters = [], Paginator $paginator = null): Result { - return $this->query('POST', $path, $parameters, $paginator); + return $this->request('POST', $path, $payload, $parameters, $paginator); } /** * @throws GuzzleException * @throws RequestRequiresClientIdException */ - public function delete(string $path = '', array $parameters = [], Paginator $paginator = null): Result + public function put(string $path, array $payload = [], array $parameters = [], Paginator $paginator = null): Result { - return $this->query('DELETE', $path, $parameters, $paginator); + return $this->request('PUT', $path, $payload, $parameters, $paginator); } /** * @throws GuzzleException * @throws RequestRequiresClientIdException */ - public function put(string $path = '', array $parameters = [], Paginator $paginator = null): Result + public function delete(string $path, array $payload = [], array $parameters = [], Paginator $paginator = null): Result { - return $this->query('PUT', $path, $parameters, $paginator); - } - - /** - * @throws GuzzleException - * @throws RequestRequiresClientIdException - */ - public function json(string $method, string $path = '', array $body = null): Result - { - if ($body) { - $body = json_encode(['data' => $body]); - } - - return $this->query($method, $path, [], null, $body); + return $this->request('DELETE', $path, $payload, $parameters, $paginator); } } diff --git a/src/Id/ApiOperations/Delete.php b/src/Id/ApiOperations/Delete.php index 1c46638..0b27aca 100644 --- a/src/Id/ApiOperations/Delete.php +++ b/src/Id/ApiOperations/Delete.php @@ -2,10 +2,18 @@ namespace Anikeen\Id\ApiOperations; +use Anikeen\Id\Exceptions\RequestRequiresClientIdException; use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Result; +use GuzzleHttp\Exception\GuzzleException; trait Delete { - abstract public function delete(string $path = '', array $parameters = [], Paginator $paginator = null): Result; + /** + * Delete a resource from the API. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + abstract public function delete(string $path, array $payload = [], array $parameters = [], Paginator $paginator = null): Result; } \ No newline at end of file diff --git a/src/Id/ApiOperations/Get.php b/src/Id/ApiOperations/Get.php index e732705..05bf645 100644 --- a/src/Id/ApiOperations/Get.php +++ b/src/Id/ApiOperations/Get.php @@ -2,10 +2,18 @@ namespace Anikeen\Id\ApiOperations; +use Anikeen\Id\Exceptions\RequestRequiresClientIdException; use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Result; +use GuzzleHttp\Exception\GuzzleException; trait Get { - abstract public function get(string $path = '', array $parameters = [], Paginator $paginator = null): Result; + /** + * Get a resource from the API. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + abstract public function get(string $path, array $parameters = [], Paginator $paginator = null): Result; } \ No newline at end of file diff --git a/src/Id/ApiOperations/Post.php b/src/Id/ApiOperations/Post.php index b35963b..9d62c73 100644 --- a/src/Id/ApiOperations/Post.php +++ b/src/Id/ApiOperations/Post.php @@ -2,10 +2,18 @@ namespace Anikeen\Id\ApiOperations; +use Anikeen\Id\Exceptions\RequestRequiresClientIdException; use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Result; +use GuzzleHttp\Exception\GuzzleException; trait Post { - abstract public function post(string $path = '', array $parameters = [], Paginator $paginator = null): Result; + /** + * Make a POST request to the API. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + abstract public function post(string $path, array $payload = [], array $parameters = [], Paginator $paginator = null): Result; } \ No newline at end of file diff --git a/src/Id/ApiOperations/Put.php b/src/Id/ApiOperations/Put.php index fac98bc..e879d00 100644 --- a/src/Id/ApiOperations/Put.php +++ b/src/Id/ApiOperations/Put.php @@ -2,10 +2,18 @@ namespace Anikeen\Id\ApiOperations; +use Anikeen\Id\Exceptions\RequestRequiresClientIdException; use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Result; +use GuzzleHttp\Exception\GuzzleException; trait Put { - abstract public function put(string $path = '', array $parameters = [], Paginator $paginator = null): Result; + /** + * Make a PUT request to the API. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + abstract public function put(string $path, array $payload = [], array $parameters = [], Paginator $paginator = null): Result; } \ No newline at end of file diff --git a/src/Id/ApiOperations/Request.php b/src/Id/ApiOperations/Request.php new file mode 100644 index 0000000..d9c0795 --- /dev/null +++ b/src/Id/ApiOperations/Request.php @@ -0,0 +1,19 @@ +config->get('session'); - $expiration = Carbon::now()->addMinutes($config['lifetime']); + $expiration = Carbon::now()->addMinutes((int)$config['lifetime']); return new Cookie( AnikeenId::cookie(), diff --git a/src/Id/Auth/TokenGuard.php b/src/Id/Auth/TokenGuard.php index bb55b25..a3e1f26 100644 --- a/src/Id/Auth/TokenGuard.php +++ b/src/Id/Auth/TokenGuard.php @@ -3,8 +3,8 @@ namespace Anikeen\Id\Auth; use Anikeen\Id\AnikeenId; +use Anikeen\Id\HasAnikeenTokens; use Anikeen\Id\Helpers\JwtParser; -use Anikeen\Id\Traits\HasAnikeenTokens; use Exception; use Firebase\JWT\JWT; use Firebase\JWT\Key; diff --git a/src/Id/Billable.php b/src/Id/Billable.php new file mode 100644 index 0000000..e70b977 --- /dev/null +++ b/src/Id/Billable.php @@ -0,0 +1,58 @@ +userData) { + $this->userData = $this->request('GET', 'v1/user')->data; + } + return $this->userData; + } + + /** + * Make a request to the Anikeen API. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + protected function request(string $method, string $path, null|array $payload = null, array $parameters = [], Paginator $paginator = null): Result + { + $anikeenId = new AnikeenId(); + $anikeenId->withToken($this->{AnikeenId::getAccessTokenKey()}); + + return $anikeenId->request($method, $path, $payload, $parameters, $paginator); + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesBalance.php b/src/Id/Concerns/ManagesBalance.php new file mode 100644 index 0000000..63e4434 --- /dev/null +++ b/src/Id/Concerns/ManagesBalance.php @@ -0,0 +1,53 @@ +getUserData()->current_balance; + } + + /** + * Get charges from the current user. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function charges(): float + { + return $this->getUserData()->current_charges; + } + + /** + * Charge given amount from bank to current user. + * + * @param float $amount Amount to charge in euros. + * @param string $paymentMethodId Payment method ID. + * @param array $options Additional options for the charge. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function charge(float $amount, string $paymentMethodId, array $options = []): Result + { + return $this->request('POST', 'billing/charge', [ + 'amount' => $amount, + 'payment_method_id' => $paymentMethodId, + 'options' => $options, + ]); + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesInvoices.php b/src/Id/Concerns/ManagesInvoices.php new file mode 100644 index 0000000..f69baf3 --- /dev/null +++ b/src/Id/Concerns/ManagesInvoices.php @@ -0,0 +1,48 @@ +request('GET', 'v1/invoices'); + } + + /** + * Get given invoice from the current user. + * + * @param string $invoiceId The invoice ID + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function invoice(string $invoiceId): Result + { + return $this->request('GET', sprintf('v1/invoices/%s', $invoiceId)); + } + + /** + * Get download url from given invoice. + * + * @param string $invoiceId The invoice ID + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function getInvoiceDownloadUrl(string $invoiceId): string + { + return $this->request('PUT', sprintf('v1/invoices/%s', $invoiceId))->data->download_url; + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesOrders.php b/src/Id/Concerns/ManagesOrders.php new file mode 100644 index 0000000..97cd516 --- /dev/null +++ b/src/Id/Concerns/ManagesOrders.php @@ -0,0 +1,260 @@ +request('GET', 'v1/orders'); + } + + /** + * Creates a new order for the current user. + * + * VAT is calculated based on the billing address and shown in the order response. + * + * @param array{ + * billing_address: array{ + * company_name: null|string, + * first_name: null|string, + * last_name: null|string, + * address: null|string, + * address_2: null|string, + * house_number: null|string, + * city: null|string, + * state: null|string, + * postal_code: null|string, + * country_iso: string, + * phone_number: null|string, + * email: null|string + * }, + * shipping_address: null|array{ + * company_name: null|string, + * first_name: string, + * last_name: string, + * address: null|string, + * address_2: string, + * house_number: null|string, + * city: string, + * state: string, + * postal_code: string, + * country_iso: string, + * phone_number: null|string, + * email: null|string + * }, + * items: array + * } $attributes The order data: + * - billing_address: Billing address (ISO 3166-1 alpha-2 country code) + * - shipping_address: Shipping address (first name, last name, ISO 3166-1 alpha-2 country code) + * - items: Array of order items (each with type, name, price, unit, units, and quantity) + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function createOrder(array $attributes = []): Result + { + return $this->request('POST', 'v1/orders', $attributes); + } + + /** + * Get given order from the current user. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function order(string $orderId): Result + { + return $this->request('GET', sprintf('v1/orders/%s', $orderId)); + } + + /** + * Update given order from the current user. + * + * VAT is calculated based on the billing address and shown in the order response. + * + * @param string $orderId The order ID. + * @param array{ + * billing_address: array{ + * company_name: null|string, + * first_name: null|string, + * last_name: null|string, + * address: null|string, + * address_2: null|string, + * house_number: null|string, + * city: null|string, + * state: null|string, + * postal_code: null|string, + * country_iso: string, + * phone_number: null|string, + * email: null|string + * }, + * shipping_address: null|array{ + * company_name: null|string, + * first_name: string, + * last_name: string, + * address: null|string, + * address_2: string, + * house_number: null|string, + * city: string, + * state: string, + * postal_code: string, + * country_iso: string, + * phone_number: null|string, + * email: null|string + * } + * } $attributes The order data: + * - billing_address: Billing address (ISO 3166-1 alpha-2 country code) + * - shipping_address: Shipping address (first name, last name, ISO 3166-1 alpha-2 country code) + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function updateOrder(string $orderId, array $attributes = []): Result + { + return $this->request('PUT', sprintf('v1/orders/%s', $orderId), $attributes); + } + + /** + * Checkout given order from the current user. + * + * @param string $orderId The order ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function checkoutOrder(string $orderId): Result + { + return $this->request('PUT', sprintf('v1/orders/%s/checkout', $orderId)); + } + + /** + * Revoke given order from the current user. + * + * @param string $orderId The order ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function revokeOrder(string $orderId): Result + { + return $this->request('PUT', sprintf('v1/orders/%s/revoke', $orderId)); + } + + /** + * Delete given order from the current user. + * + * @param string $orderId The order ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function deleteOrder(string $orderId): Result + { + return $this->request('DELETE', sprintf('v1/orders/%s', $orderId)); + } + + /** + * Get order items from given order. + * + * @param string $orderId The order ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function orderItems(string $orderId): Result + { + return $this->request('GET', sprintf('v1/orders/%s/items', $orderId)); + } + + /** + * Create a new order item for given order. + * + * VAT is calculated based on the billing address and shown in the order item response. + * + * @param string $orderId The order ID. + * @param array{ + * items: array + * } $attributes The order data: + * - items: Array of order items, each with type, name, description, price, unit, and quantity + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function createOrderItem(string $orderId, array $attributes = []): Result + { + return $this->request('POST', sprintf('v1/orders/%s', $orderId), $attributes); + } + + /** + * Get given order item from given order. + * + * @param string $orderId The order ID. + * @param string $orderItemId The order item ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function orderItem(string $orderId, string $orderItemId): Result + { + return $this->request('GET', sprintf('v1/orders/%s/items/%s', $orderId, $orderItemId)); + } + + /** + * Update given order item from given order. + * + * VAT is calculated based on the billing address and shown in the order item response. + * + * @param string $orderId The order ID. + * @param string $orderItemId The order item ID. + * @param array{ + * items: array + * } $attributes The order data: + * - items: Array of order items, each with type, name, description, price, unit, and quantity + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function updateOrderItem(string $orderId, string $orderItemId, array $attributes = []): Result + { + return $this->request('PUT', sprintf('v1/orders/%s/items/%s', $orderId, $orderItemId), $attributes); + } + + /** + * Delete given order item from given order. + * + * @param string $orderId The order ID. + * @param string $orderItemId The order item ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function deleteOrderItem(string $orderId, string $orderItemId): Result + { + return $this->request('DELETE', sprintf('v1/orders/%s/items/%s', $orderId, $orderItemId)); + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesPaymentMethods.php b/src/Id/Concerns/ManagesPaymentMethods.php new file mode 100644 index 0000000..5775758 --- /dev/null +++ b/src/Id/Concerns/ManagesPaymentMethods.php @@ -0,0 +1,87 @@ +paymentMethods()->count() > 0; + } + + /** + * Get payment methods from the current user. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function paymentMethods(): Result + { + return $this->request('GET', 'v1/payment-methods'); + } + + /** + * Get default payment method from the current user. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function hasDefaultPaymentMethod(): bool + { + return $this->defaultPaymentMethod()->count() > 0; + } + + /** + * Get default payment method from the current user. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function defaultPaymentMethod(): Result + { + return $this->request('GET', 'v1/payment-methods/default'); + } + + /** + * Get billing portal URL for the current user. + * + * @param string $returnUrl The URL to redirect to after the user has finished in the billing portal. + * @param array $options Additional options for the billing portal. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function billingPortalUrl(string $returnUrl, array $options): string + { + return $this->request('POST', 'v1/stripe/billing-portal', [ + 'return_url' => $returnUrl, + 'options' => $options, + ])->data->url; + } + + /** + * Create a new setup intent. + * + * @param array $options Additional options for the setup intent. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function createSetupIntent(array $options = []): Result + { + return $this->request('POST', 'v1/payment-methods', [ + 'options' => $options, + ]); + } +} diff --git a/src/Id/Concerns/ManagesPricing.php b/src/Id/Concerns/ManagesPricing.php new file mode 100644 index 0000000..4890d49 --- /dev/null +++ b/src/Id/Concerns/ManagesPricing.php @@ -0,0 +1,39 @@ + + * } $attributes The order data: + * - country_iso: ISO 3166-1 alpha-2 country code + * - items: Array of order items (each with type, name, price, unit, units, and quantity) + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function createOrderPreview(array $attributes = []): Result + { + return $this->post('v1/orders/preview', $attributes); + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesSshKeys.php b/src/Id/Concerns/ManagesSshKeys.php new file mode 100644 index 0000000..3f6df84 --- /dev/null +++ b/src/Id/Concerns/ManagesSshKeys.php @@ -0,0 +1,54 @@ +get(sprintf('v1/users/%s/ssh-keys/json', $sskKeyId)); + } + + /** + * Creates ssh key for the currently authed user. + * + * @param string $publicKey The public key to be added + * @param string|null $name The name of the key (optional) + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function createSshKey(string $publicKey, string $name = null): Result + { + return $this->post('v1/ssh-keys', [ + 'public_key' => $publicKey, + 'name' => $name, + ]); + } + + /** + * Deletes a given ssh key for the currently authed user. + * + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function deleteSshKey(int $sshKeyId): Result + { + return $this->delete(sprintf('v1/ssh-keys/%s', $sshKeyId)); + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesSubscriptions.php b/src/Id/Concerns/ManagesSubscriptions.php new file mode 100644 index 0000000..4866cc6 --- /dev/null +++ b/src/Id/Concerns/ManagesSubscriptions.php @@ -0,0 +1,103 @@ +request('GET', 'v1/subscriptions'); + } + + /** + * Get given subscription from the current user. + * + * @param string $subscriptionId The subscription ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function subscription(string $subscriptionId): Result + { + return $this->request('GET', sprintf('v1/subscriptions/%s', $subscriptionId)); + } + + /** + * Create a new subscription for the current user. + * + * @param array{ + * name: null, + * description: string, + * unit: string, + * price: float, + * vat: null|float, + * payload: null|array, + * ends_at: null|string, + * webhook_url: null|string, + * webhook_secret: null|string + * } $attributes The subscription data: + * - name: The name + * - description: The description + * - unit: The unit (e.g. "hour", "day", "week", "month", "year") + * - price: The price per unit + * - vat: The VAT (optional) + * - payload: The payload (optional) + * - ends_at: The end date (optional) + * - webhook_url: The webhook URL (optional) + * - webhook_secret: The webhook secret (optional) + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function createSubscription(array $attributes): Result + { + return $this->request('POST', 'v1/subscriptions', $attributes); + } + + /** + * Force given subscription to check out (trusted apps only). + * + * @param string $subscriptionId The subscription ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function checkoutSubscription(string $subscriptionId): Result + { + return $this->request('PUT', sprintf('v1/subscriptions/%s/checkout', $subscriptionId)); + } + + /** + * Revoke a given running subscription from the current user. + * + * @param string $subscriptionId The subscription ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function revokeSubscription(string $subscriptionId): Result + { + return $this->request('PUT', sprintf('v1/subscriptions/%s/revoke', $subscriptionId)); + } + + /** + * Resume a given running subscription from the current user. + * + * @param string $subscriptionId The subscription ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function resumeSubscription(string $subscriptionId): Result + { + return $this->request('PUT', sprintf('v1/subscriptions/%s/resume', $subscriptionId)); + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesTaxation.php b/src/Id/Concerns/ManagesTaxation.php new file mode 100644 index 0000000..4abde3b --- /dev/null +++ b/src/Id/Concerns/ManagesTaxation.php @@ -0,0 +1,23 @@ +getUserData()->vat; + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesTransactions.php b/src/Id/Concerns/ManagesTransactions.php new file mode 100644 index 0000000..2488f99 --- /dev/null +++ b/src/Id/Concerns/ManagesTransactions.php @@ -0,0 +1,49 @@ +request('GET', 'v1/transactions'); + } + + /** + * Create a new transaction for the current user. + * + * @param array $attributes The attributes for the transaction. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + * @todo Add type hinting for the attributes array. + */ + public function createTransaction(array $attributes = []): Result + { + return $this->request('POST', 'v1/transactions', $attributes); + } + + /** + * Get given transaction from current current user. + * + * @param string $transactionId The transaction ID. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function transaction(string $transactionId): Result + { + return $this->request('GET', sprintf('v1/transactions/%s', $transactionId)); + } +} \ No newline at end of file diff --git a/src/Id/Concerns/ManagesUsers.php b/src/Id/Concerns/ManagesUsers.php new file mode 100644 index 0000000..df68d2f --- /dev/null +++ b/src/Id/Concerns/ManagesUsers.php @@ -0,0 +1,63 @@ +get('v1/user'); + } + + /** + * Creates a new user on behalf of the current user. + * + * @param array{ + * first_name: null|string, + * last_name: null|string, + * username: null|string, + * email: string, + * password: null|string + * } $attributes The user data + * - first_name: The first name (optional) + * - last_name: The last name (optional) + * - username: The username (optional) + * - email: The email (required) + * - password: The password (optional, can be reset by the user if not provided) + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function createUser(array $attributes): Result + { + return $this->post('v1/users', $attributes); + } + + /** + * Checks if the given email exists. + * + * @param string $email The email to check. + * @throws RequestRequiresClientIdException + * @throws GuzzleException + */ + public function isEmailExisting(string $email): Result + { + return $this->post('v1/users/check', [ + 'email' => $email, + ]); + } +} diff --git a/src/Id/Traits/HasAnikeenTokens.php b/src/Id/HasAnikeenTokens.php similarity index 92% rename from src/Id/Traits/HasAnikeenTokens.php rename to src/Id/HasAnikeenTokens.php index 7ebd96a..0ed969e 100644 --- a/src/Id/Traits/HasAnikeenTokens.php +++ b/src/Id/HasAnikeenTokens.php @@ -1,6 +1,6 @@ pagination = $pagination; + $this->links = $links; + $this->meta = $meta; } /** - * Create Paginator from Result object. + * Create Paginator from a Result instance. */ public static function from(Result $result): self { - return new self($result->pagination); + return new self($result->links, $result->meta); } /** - * Return the current active cursor. + * Return the cursor value (page number) based on the last set action. */ public function cursor(): string { - return $this->pagination->cursor; + switch ($this->action) { + case 'first': + return '1'; + + case 'after': + // Try parsing from 'next' link + if ($this->links && !empty($this->links->next)) { + return $this->parsePageFromUrl($this->links->next); + } + // Fallback to current_page + 1 + return isset($this->meta->current_page) + ? (string)($this->meta->current_page + 1) + : '1'; + + case 'before': + if ($this->links && !empty($this->links->prev)) { + return $this->parsePageFromUrl($this->links->prev); + } + // Fallback to current_page - 1 + return isset($this->meta->current_page) + ? (string)($this->meta->current_page - 1) + : '1'; + + default: + // Default to current page + return isset($this->meta->current_page) + ? (string)$this->meta->current_page + : '1'; + } } /** - * Set the Paginator to fetch the next set of results. + * Parse the 'page' query parameter from a URL. + */ + private function parsePageFromUrl(string $url): string + { + $parts = parse_url($url); + if (empty($parts['query'])) { + return '1'; + } + parse_str($parts['query'], $vars); + return $vars['page'] ?? '1'; + } + + /** + * Fetch the first page. */ public function first(): self { $this->action = 'first'; - return $this; } /** - * Set the Paginator to fetch the first set of results. + * Fetch the next page (after). */ public function next(): self { $this->action = 'after'; - return $this; } /** - * Set the Paginator to fetch the last set of results. + * Fetch the previous page (before). */ public function back(): self { $this->action = 'before'; - return $this; } } \ No newline at end of file diff --git a/src/Id/Http/Middleware/CreateFreshApiToken.php b/src/Id/Http/Middleware/CreateFreshApiToken.php index 4491c0b..961a3b5 100644 --- a/src/Id/Http/Middleware/CreateFreshApiToken.php +++ b/src/Id/Http/Middleware/CreateFreshApiToken.php @@ -2,35 +2,62 @@ namespace Anikeen\Id\Http\Middleware; +use Anikeen\Id\AnikeenId; use Anikeen\Id\ApiTokenCookieFactory; -use Anikeen\Id\Facades\AnikeenId; use Closure; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CreateFreshApiToken { + /** + * The API token cookie factory instance. + * + * @var ApiTokenCookieFactory + */ + protected $cookieFactory; + /** * The authentication guard. * * @var string */ - protected string $guard; + protected $guard; /** * Create a new middleware instance. * + * @param ApiTokenCookieFactory $cookieFactory * @return void */ - public function __construct(protected ApiTokenCookieFactory $cookieFactory) + public function __construct(ApiTokenCookieFactory $cookieFactory) { - // + $this->cookieFactory = $cookieFactory; + } + + /** + * Specify the guard for the middleware. + * + * @param string|null $guard + * @return string + */ + public static function using($guard = null) + { + $guard = is_null($guard) ? '' : ':' . $guard; + + return static::class . $guard; } /** * Handle an incoming request. + * + * @param Request $request + * @param Closure $next + * @param string|null $guard + * @return mixed */ - public function handle(Request $request, Closure $next, string $guard = null): mixed + public function handle($request, Closure $next, $guard = null) { $this->guard = $guard; @@ -47,8 +74,12 @@ class CreateFreshApiToken /** * Determine if the given request should receive a fresh token. + * + * @param Request $request + * @param Response $response + * @return bool */ - protected function shouldReceiveFreshToken(Request $request, Response $response): bool + protected function shouldReceiveFreshToken($request, $response) { return $this->requestShouldReceiveFreshToken($request) && $this->responseShouldReceiveFreshToken($response); @@ -56,25 +87,37 @@ class CreateFreshApiToken /** * Determine if the request should receive a fresh token. + * + * @param Request $request + * @return bool */ - protected function requestShouldReceiveFreshToken(Request $request): bool + protected function requestShouldReceiveFreshToken($request) { return $request->isMethod('GET') && $request->user($this->guard); } /** * Determine if the response should receive a fresh token. + * + * @param Response $response + * @return bool */ - protected function responseShouldReceiveFreshToken(Response $response): bool + protected function responseShouldReceiveFreshToken($response) { - return !$this->alreadyContainsToken($response); + return ($response instanceof Response || + $response instanceof JsonResponse) && + !$this->alreadyContainsToken($response); } /** * Determine if the given response already contains an API token. + * * This avoids us overwriting a just "refreshed" token. + * + * @param Response $response + * @return bool */ - protected function alreadyContainsToken(Response $response): bool + protected function alreadyContainsToken($response) { foreach ($response->headers->getCookies() as $cookie) { if ($cookie->getName() === AnikeenId::cookie()) { @@ -84,4 +127,4 @@ class CreateFreshApiToken return false; } -} \ No newline at end of file +} diff --git a/src/Id/Traits/OauthTrait.php b/src/Id/OauthTrait.php similarity index 94% rename from src/Id/Traits/OauthTrait.php rename to src/Id/OauthTrait.php index 174154a..0d4da65 100644 --- a/src/Id/Traits/OauthTrait.php +++ b/src/Id/OauthTrait.php @@ -1,9 +1,8 @@ publishes([ - dirname(__DIR__, 3) . '/config/anikeen-id.php' => config_path('anikeen-id.php'), - ], 'config'); + // } /** @@ -31,7 +29,6 @@ class AnikeenIdServiceProvider extends ServiceProvider */ public function register(): void { - $this->mergeConfigFrom(dirname(__DIR__, 3) . '/config/anikeen-id.php', 'anikeen-id'); $this->app->singleton(Contracts\AppTokenRepository::class, Repository\AppTokenRepository::class); $this->app->singleton(AnikeenId::class, function () { return new AnikeenId; diff --git a/src/Id/Result.php b/src/Id/Result.php index d3755fc..1186680 100644 --- a/src/Id/Result.php +++ b/src/Id/Result.php @@ -9,67 +9,112 @@ use stdClass; class Result { - /** - * Query successful. + * Was the API call successful? */ public bool $success = false; /** - * Query result data. + * Response data: either an array of items (paginated) or a single object (non-paginated) */ - public array $data = []; + public mixed $data = []; /** - * Total amount of result data. + * Total number of items: uses meta.total, root total, or falls back to count/data existence */ public int $total = 0; /** - * Status Code. + * HTTP status code */ public int $status = 0; /** - * AnikeenId response pagination cursor. + * Pagination links (first, last, prev, next) as stdClass or null */ - public ?stdClass $pagination; + public ?stdClass $links = null; /** - * Original AnikeenId instance. - * - * @var AnikeenId + * Pagination meta (current_page, last_page etc.) as stdClass or null + */ + public ?stdClass $meta = null; + + /** + * Paginator helper to retrieve next/prev pages + */ + public ?Paginator $paginator = null; + + /** + * Reference to the original AnikeenId client */ public AnikeenId $anikeenId; - public function __construct(public ?ResponseInterface $response, public ?Exception $exception = null, public ?Paginator $paginator = null) - { + /** + * Constructor + * + * @param ResponseInterface|null $response + * @param Exception|null $exception + * @param AnikeenId $anikeenId + */ + public function __construct( + public ?ResponseInterface $response, + public ?Exception $exception, + AnikeenId $anikeenId + ) { + $this->anikeenId = $anikeenId; $this->success = $exception === null; $this->status = $response ? $response->getStatusCode() : 500; - $jsonResponse = $response ? @json_decode($response->getBody()->getContents(), false) : null; - if ($jsonResponse !== null) { - $this->setProperty($jsonResponse, 'data'); - $this->setProperty($jsonResponse, 'total'); - $this->setProperty($jsonResponse, 'pagination'); - $this->paginator = Paginator::from($this); + + $raw = $response ? (string) $response->getBody() : null; + $json = $raw ? @json_decode($raw, false) : null; + + if ($json !== null) { + // Pagination info + $this->links = $json->links ?? null; + $this->meta = $json->meta ?? null; + + // Determine data shape + if (isset($json->data)) { + if ($this->links !== null || $this->meta !== null) { + // Paginated: always array + $this->data = is_array($json->data) ? $json->data : [$json->data]; + } else { + // Non-paginated: single object + $this->data = $json->data; + } + } else { + // No 'data' key: treat entire payload + if ($this->links !== null || $this->meta !== null) { + // Paginated but missing data key: fallback to empty array + $this->data = []; + } else { + $this->data = $json; + } + } + + // Total items + if (isset($json->meta->total)) { + $this->total = (int) $json->meta->total; + } elseif (isset($json->total)) { + $this->total = (int) $json->total; + } else { + // count array or single object + if (is_array($this->data)) { + $this->total = count($this->data); + } elseif ($this->data !== null) { + $this->total = 1; + } + } + + // Initialize paginator only if pagination present + if ($this->links !== null || $this->meta !== null) { + $this->paginator = Paginator::from($this); + } } } /** - * Sets a class attribute by given JSON Response Body. - */ - private function setProperty(stdClass $jsonResponse, string $responseProperty, string $attribute = null): void - { - $classAttribute = $attribute ?? $responseProperty; - if (property_exists($jsonResponse, $responseProperty)) { - $this->{$classAttribute} = $jsonResponse->{$responseProperty}; - } elseif ($responseProperty === 'data') { - $this->{$classAttribute} = $jsonResponse; - } - } - - /** - * Returns whether the query was successfully. + * Was the request successful? */ public function success(): bool { @@ -77,47 +122,46 @@ class Result } /** - * Returns the last HTTP or API error. + * Get last error message */ public function error(): string { - // TODO Switch Exception response parsing to this->data - if ($this->exception === null || !$this->exception->hasResponse()) { + if ($this->exception === null || !method_exists($this->exception, 'getResponse')) { return 'Anikeen ID API Unavailable'; } - $exception = (string)$this->exception->getResponse()->getBody(); - $exception = @json_decode($exception); - if (property_exists($exception, 'message') && !empty($exception->message)) { - return $exception->message; + $resp = $this->exception->getResponse(); + $body = $resp ? (string) $resp->getBody() : null; + $err = $body ? @json_decode($body) : null; + if (isset($err->message) && $err->message !== '') { + return $err->message; } - return $this->exception->getMessage(); } /** - * Shifts the current result (Use for single user/video etc. query). + * For paginated data: shift first element; for single object: return it */ public function shift(): mixed { - if (!empty($this->data)) { - $data = $this->data; - - return array_shift($data); + if (is_array($this->data)) { + return array_shift($this->data); } - - return null; + return $this->data; } /** - * Return the current count of items in dataset. + * Count of items in data */ public function count(): int { - return count($this->data); + if (is_array($this->data)) { + return count($this->data); + } + return $this->data !== null ? 1 : 0; } /** - * Set the Paginator to fetch the next set of results. + * Fetch next page paginator */ public function next(): ?Paginator { @@ -125,7 +169,7 @@ class Result } /** - * Set the Paginator to fetch the last set of results. + * Fetch previous page paginator */ public function back(): ?Paginator { @@ -133,71 +177,62 @@ class Result } /** - * Get rate limit information. + * Rate limit info from headers */ public function rateLimit(string $key = null): array|int|string|null { if (!$this->response) { return null; } - $rateLimit = [ - 'limit' => (int)$this->response->getHeaderLine('X-RateLimit-Limit'), - 'remaining' => (int)$this->response->getHeaderLine('X-RateLimit-Remaining'), - 'reset' => (int)$this->response->getHeaderLine('Retry-After'), + $info = [ + 'limit' => (int) $this->response->getHeaderLine('X-RateLimit-Limit'), + 'remaining' => (int) $this->response->getHeaderLine('X-RateLimit-Remaining'), + 'reset' => (int) $this->response->getHeaderLine('Retry-After'), ]; - if ($key === null) { - return $rateLimit; - } - - return $rateLimit[$key]; + return $key ? ($info[$key] ?? null) : $info; } /** - * Insert users in data response. + * Insert related users into each data item (for arrays) */ public function insertUsers(string $identifierAttribute = 'user_id', string $insertTo = 'user'): self { - $data = $this->data; - $userIds = collect($data)->map(function ($item) use ($identifierAttribute) { - return $item->{$identifierAttribute}; - })->toArray(); - if (count($userIds) === 0) { + if (!is_array($this->data)) { return $this; } - $users = collect($this->anikeenId->getUsersByIds($userIds)->data); - $dataWithUsers = collect($data)->map(function ($item) use ($users, $identifierAttribute, $insertTo) { - $item->$insertTo = $users->where('id', $item->{$identifierAttribute})->first(); - - return $item; - }); - $this->data = $dataWithUsers->toArray(); - + $ids = array_map(fn($item) => $item->{$identifierAttribute} ?? null, $this->data); + $ids = array_filter($ids); + if (empty($ids)) { + return $this; + } + $users = $this->anikeenId->getUsersByIds($ids)->data; + foreach ($this->data as &$item) { + $item->{$insertTo} = collect($users)->firstWhere('id', $item->{$identifierAttribute}); + } return $this; } /** - * Set the Paginator to fetch the first set of results. + * Fetch first page paginator */ public function first(): ?Paginator { return $this->paginator?->first(); } + /** + * Original response + */ public function response(): ?ResponseInterface { return $this->response; } - public function dump(): void - { - dump($this->data()); - } - /** - * Get the response data, also available as public attribute. + * Access raw data */ - public function data(): array + public function data(): mixed { return $this->data; } -} \ No newline at end of file +} diff --git a/src/Id/Traits/SshKeysTrait.php b/src/Id/Traits/SshKeysTrait.php deleted file mode 100644 index 48d20ec..0000000 --- a/src/Id/Traits/SshKeysTrait.php +++ /dev/null @@ -1,42 +0,0 @@ -get("v1/users/$id/ssh-keys/json", [], null); - } - - /** - * Creates ssh key for the currently authed user - */ - public function createSshKey(string $publicKey, string $name = null): Result - { - return $this->post('v1/ssh-keys', [ - 'public_key' => $publicKey, - 'name' => $name, - ]); - } - - /** - * Deletes a given ssh key for the currently authed user - */ - public function deleteSshKey(int $id): Result - { - return $this->delete("v1/ssh-keys/$id", []); - } -} \ No newline at end of file diff --git a/src/Id/Traits/UsersTrait.php b/src/Id/Traits/UsersTrait.php deleted file mode 100644 index de8ddfc..0000000 --- a/src/Id/Traits/UsersTrait.php +++ /dev/null @@ -1,39 +0,0 @@ -get('v1/user'); - } - - /** - * Creates a new user on behalf of the current user. - */ - public function createUser(array $parameters): Result - { - return $this->post('v1/users', $parameters); - } - - /** - * Checks if the given email exists. - */ - public function isEmailExisting(string $email): Result - { - return $this->post('v1/users/check', [ - 'email' => $email, - ]); - } -}