refactored code

Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
This commit is contained in:
2025-04-28 04:47:50 +02:00
parent 05e8cca347
commit 7f908f4e6a
33 changed files with 1577 additions and 463 deletions

230
README.md
View File

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

View File

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

View File

@@ -1,8 +0,0 @@
<?php
return [
'client_key' => env('ANIKEEN_ID_KEY'),
'client_secret' => env('ANIKEEN_ID_SECRET'),
'redirect_url' => env('ANIKEEN_ID_REDIRECT_URI'),
'base_url' => env('ANIKEEN_ID_BASE_URL'),
];

View File

@@ -3,83 +3,109 @@
require __DIR__ . '/../vendor/autoload.php';
use Anikeen\Id\AnikeenId;
use Anikeen\Id\Billable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
$markdown = collect(class_uses(AnikeenId::class))
->map(function ($trait) {
// Liste der Klassen, die ausgewertet werden sollen
$classes = [
AnikeenId::class,
Billable::class,
];
$title = str_replace('Trait', '', Arr::last(explode('\\', $trait)));
$allMarkdown = collect($classes)
->map(function (string $class) {
$className = Arr::last(explode('\\', $class));
$markdown = "## {$className}\n\n";
$methods = [];
$reflection = new ReflectionClass($trait);
collect($reflection->getMethods())
->reject(function (ReflectionMethod $method) {
return $method->isAbstract();
// alle Traits der Klasse, außer denen aus ApiOperations
$traits = collect(class_uses($class) ?: [])
->reject(function (string $trait) {
return Str::contains($trait, 'ApiOperations\\');
})
->reject(function (ReflectionMethod $method) {
return $method->isPrivate() || $method->isProtected();
->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;
})
->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 .= '```';
->implode("\n");
return $markdown;
})->join(PHP_EOL . PHP_EOL);
$markdown = str_replace("array (\n)", '[]', $markdown);
$content = file_get_contents(__DIR__ . '/../README.stub');
$content = str_replace('<!-- GENERATED-DOCS -->', $markdown, $content);
})
->implode("\n\n");
// README zusammenbauen und schreiben
$stub = file_get_contents(__DIR__ . '/../README.stub');
$content = str_replace('<!-- GENERATED-DOCS -->', $allMarkdown, $stub);
file_put_contents(__DIR__ . '/../README.md', $content);

View File

@@ -23,7 +23,7 @@
<php>
<env name="ANIKEEN_ID_KEY" value="38"/>
<env name="ANIKEEN_ID_SECRET" value="2goNRF8x37HPVZVaa28ySZGVXJuksvxnxM7TcGzM"/>
<env name="ANIKEEN_ID_BASE_URL" value="https://id.anikeen.com/api/v1/"/>
<env name="ANIKEEN_ID_BASE_URL" value="https://id.anikeen.com/api/"/>
<env name="CLIENT_ACCESS_TOKEN" value="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxNSIsImp0aSI6IjAxN2QxZDg0Y2MxNjAyZTYxOGZkNjYwZGViZWVjMDY4MTk2YmYzMDk1OGMzY2RiYzBjZmJkZWFjZjFhOTUxODQzZDU1YTk3OGY2YWIxY2YzIiwiaWF0IjoxNjY0NjIzMzA4LjQyODI4MSwibmJmIjoxNjY0NjIzMzA4LjQyODI4NCwiZXhwIjoxNjgwMzQ4MTA4LjQxNjc4MSwic3ViIjoiMzkiLCJzY29wZXMiOlsiYXBpIiwicmVhZF91c2VyIl0sImNsaWVudCI6eyJ0cnVzdGVkIjpmYWxzZX19.vxnzCaU4PpOrNVHa5AnGSS6gX_RCvxIERAnHFhjTrUzRafV9mr2Cvwd-BDVYoUr10wHvxa_TJSYfnAdDuhE-MEyDv13O3dL2XGTtJNa_Rg6L6CQ0JvC3wL-lWPvGPFax9pu-_lqbA3jm5B08hc3-7tq3f2nXcxjhtkqT6TTJv1-RCAppb2HCXiUDAqANzbhyInDjOH2WvFj1OGC_AI03J3W2KRWyeFLtUne8XKPFyr9XGcPuTrqogcuuXLeUt2kcf2bXbuIV1OlgIECrDiP1Ee0F2AzPs27ZVJ2z0R0JbT6AubKhGl5_Qi27cwjOH7hT2dmjcF1mLjzpN1uChLIdSnGSoStH8VzYHnHE2I8G-owW_aadG2UmGdnRY143q6g_28f3WIZNSucBSXkwFeS_t4fylsvpxhpjYJusf5qiEU_X3YbeawYMUCFUkSD2XTIypAqMJLNZQAeJ52eyL-9fln-Bv7n9v7K9G6ieR6Tm0tsJ1PRnaQi7rA1NTFwHoQmIOW9tfMycLzT7bgSoz3ra6Ez2J7ZNuWBZNKS0O-0YfSrAWyWK5U8YRfQuSVzP2VrIU63K6RGU2c284PfQGy11kgKUNQPykirb8p7MDQ8PwrxKaylBnD6hhDgjqEh2bfsr_43DfJA0R58L1HK3BmQnxgap0C77wK1e0yNlABpN28Q"/>
</php>
</phpunit>

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Anikeen\Id\ApiOperations;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Helpers\Paginator;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait Request
{
/**
* Make a request to the API.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
abstract public function request(string $method, string $path, null|array $payload = null, array $parameters = [], Paginator $paginator = null): Result;
}

View File

@@ -10,7 +10,7 @@ trait Validation
/**
* @throws RequestRequiresMissingParametersException
*/
public function validateRequired(array $parameters, array $required)
public function validateRequired(array $parameters, array $required): void
{
if (!Arr::has($parameters, $required)) {
throw RequestRequiresMissingParametersException::fromValidateRequired($parameters, $required);

View File

@@ -25,7 +25,7 @@ class ApiTokenCookieFactory
{
$config = $this->config->get('session');
$expiration = Carbon::now()->addMinutes($config['lifetime']);
$expiration = Carbon::now()->addMinutes((int)$config['lifetime']);
return new Cookie(
AnikeenId::cookie(),

View File

@@ -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;

58
src/Id/Billable.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace Anikeen\Id;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Concerns\ManagesBalance;
use Anikeen\Id\Concerns\ManagesInvoices;
use Anikeen\Id\Concerns\ManagesOrders;
use Anikeen\Id\Concerns\ManagesPaymentMethods;
use Anikeen\Id\Concerns\ManagesSubscriptions;
use Anikeen\Id\Concerns\ManagesTaxation;
use Anikeen\Id\Concerns\ManagesTransactions;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Helpers\Paginator;
use GuzzleHttp\Exception\GuzzleException;
use stdClass;
trait Billable
{
use ManagesBalance;
use ManagesInvoices;
use ManagesOrders;
use ManagesPaymentMethods;
use ManagesSubscriptions;
use ManagesTaxation;
use ManagesTransactions;
use Request;
protected stdClass|null $userData = null;
/**
* Get the currently authenticated user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
protected function getUserData(): stdClass
{
if (!$this->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);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesBalance
{
use Request;
/**
* Get balance from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function balance(): float
{
return $this->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,
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesInvoices
{
use Request;
/**
* Get invoices from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function invoices(): Result
{
return $this->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;
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesOrders
{
use Request;
/**
* Get orders from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function orders(): Result
{
return $this->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<array{
* type: string,
* name: string,
* description: string,
* price: float|int,
* unit: string,
* units: int
* }>
* } $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<array{
* type: string,
* name: string,
* description: string,
* price: float|int,
* unit: string,
* units: int
* }>
* } $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<array{
* type: string,
* name: string,
* description: string,
* price: float|int,
* unit: string,
* units: int
* }>
* } $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));
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesPaymentMethods
{
use Request;
/**
* Check if current user has at least one payment method.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function hasPaymentMethod(): bool
{
return $this->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,
]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Post;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesPricing
{
use Post;
/**
* Make a new order preview (will not be stored into the database).
*
* VAT is calculated based on the billing address and shown in the order response.
*
* @param array{
* country_iso: string,
* items: array<array{
* type: string,
* name: string,
* description: string,
* price: float|int,
* unit: string,
* units: int
* }>
* } $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);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Delete;
use Anikeen\Id\ApiOperations\Get;
use Anikeen\Id\ApiOperations\Post;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesSshKeys
{
use Get, Post, Delete;
/**
* Get currently authed user with Bearer Token.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function sshKeysByUserId(string $sskKeyId): Result
{
return $this->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));
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesSubscriptions
{
use Request;
/**
* Get subscriptions from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function subscriptions(): Result
{
return $this->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));
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesTaxation
{
use Request;
/**
* Get VAT for the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function vat(): float
{
return $this->getUserData()->vat;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesTransactions
{
use Request;
/**
* Get transactions from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function transactions(): Result
{
return $this->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));
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Get;
use Anikeen\Id\ApiOperations\Post;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesUsers
{
use Get, Post;
/**
* Get currently authed user with Bearer Token
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function getAuthedUser(): Result
{
return $this->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,
]);
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace Anikeen\Id\Traits;
namespace Anikeen\Id;
use stdClass;
@@ -9,7 +9,7 @@ trait HasAnikeenTokens
/**
* The current access token for the authentication user.
*/
protected ?stdClass $accessToken;
protected ?stdClass $accessToken = null;
/**
* Get the current access token being used by the user.

View File

@@ -7,70 +7,120 @@ use stdClass;
class Paginator
{
/**
* Next desired action (first, after, before).
* Next desired action: 'first', 'after', 'before'.
*
* @var string|null
*/
public ?string $action = null;
/**
* AnikeenId response pagination cursor.
* Raw pagination links from API ('first','last','prev','next').
*
* @var stdClass|null
*/
private ?stdClass $pagination;
private ?stdClass $links;
/**
* Raw pagination meta from API (current_page, last_page, etc.).
*
* @var stdClass|null
*/
private ?stdClass $meta;
/**
* Constructor.
*
* @param null|stdClass $pagination AnikeenId response pagination cursor
* @param stdClass|null $links Pagination links object
* @param stdClass|null $meta Pagination meta object
*/
public function __construct(?stdClass $pagination = null)
public function __construct(?stdClass $links = null, ?stdClass $meta = null)
{
$this->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;
}
}

View File

@@ -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()) {

View File

@@ -1,9 +1,8 @@
<?php
namespace Anikeen\Id\Traits;
namespace Anikeen\Id;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\RequestException;

View File

@@ -21,9 +21,7 @@ class AnikeenIdServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->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;

View File

@@ -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,70 +177,61 @@ 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;
}

View File

@@ -1,42 +0,0 @@
<?php
namespace Anikeen\Id\Traits;
use Anikeen\Id\ApiOperations\Delete;
use Anikeen\Id\ApiOperations\Get;
use Anikeen\Id\ApiOperations\Post;
use Anikeen\Id\Result;
trait SshKeysTrait
{
use Get, Post, Delete;
/**
* Get currently authed user with Bearer Token
*/
public function getSshKeysByUserId(int $id): Result
{
return $this->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", []);
}
}

View File

@@ -1,39 +0,0 @@
<?php
namespace Anikeen\Id\Traits;
use Anikeen\Id\ApiOperations\Get;
use Anikeen\Id\Result;
trait UsersTrait
{
use Get;
/**
* Get currently authed user with Bearer Token
*/
public function getAuthedUser(): Result
{
return $this->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,
]);
}
}