11 Commits

Author SHA1 Message Date
235918f0c0 fix some issues
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-30 04:12:54 +02:00
4b23f6ddbb refactored code
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-30 03:45:10 +02:00
85702fcb2c update docs
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-29 21:50:12 +02:00
65d4b12006 update identifier
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-29 21:47:21 +02:00
5d621b7cdc update config
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-29 21:46:44 +02:00
4d6fe7c325 update provider
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-29 21:39:49 +02:00
d447a88430 add mode
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-29 21:33:02 +02:00
d9a330222b update billable
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-28 17:26:02 +02:00
21946e3a22 update docs
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-28 05:32:44 +02:00
1b96b87e1d refactored code
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-28 05:27:06 +02:00
7f908f4e6a refactored code
Signed-off-by: Maurice Preuß (envoyr) <hello@envoyr.com>
2025-04-28 04:47:50 +02:00
62 changed files with 2392 additions and 523 deletions

313
README.md
View File

@@ -4,13 +4,13 @@
[![Total Downloads](https://img.shields.io/packagist/dt/anikeen/id.svg?style=flat-square)](https://packagist.org/packages/anikeen/id) [![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) [![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 ## Table of contents
1. [Installation](#installation) 1. [Installation](#installation)
2. [Event Listener](#event-listener) 2. [Configuration](#configuration)
3. [Configuration](#configuration) 3. [General](#general)
4. [Examples](#examples) 4. [Examples](#examples)
5. [Documentation](#documentation) 5. [Documentation](#documentation)
6. [Development](#Development) 6. [Development](#Development)
@@ -21,56 +21,93 @@ PHP Anikeen ID API Client for Laravel 10+
composer require anikeen/id 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.
```
/**
* 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',
],
];
```
## Configuration ## Configuration
Copy configuration to config folder: Add environmental variables to your `.env` file:
```
$ php artisan vendor:publish --provider="Anikeen\Id\Providers\AnikeenIdServiceProvider"
```
Add environmental variables to your `.env`
``` ```
ANIKEEN_ID_KEY= ANIKEEN_ID_KEY=
ANIKEEN_ID_SECRET= ANIKEEN_ID_SECRET=
ANIKEEN_ID_REDIRECT_URI=http://localhost ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback
```
To switch from `production` to `staging` use following variable:
```
ANIKEEN_ID_MODE=staging
``` ```
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. 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.
**Add to `config/services.php`:** Add to `config/services.php` file:
```php ```php
'anikeen-id' => [ 'anikeen' => [
'mode' => env('ANIKEEN_ID_MODE'),
'client_id' => env('ANIKEEN_ID_KEY'), 'client_id' => env('ANIKEEN_ID_KEY'),
'client_secret' => env('ANIKEEN_ID_SECRET'), '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'),
], ],
``` ```
## Implementing Auth ### Event Listener
In Laravel 11, the default EventServiceProvider provider was removed. Instead, add the listener using the listen method on the Event facade, in your `AppServiceProvider` boot method:
```php
public function boot(): void
{
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('anikeen', \Anikeen\Id\Socialite\Provider::class);
});
}
```
### Registering Middleware
Append it to the global middleware stack in your application's `bootstrap/app.php` file:
```php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Anikeen\Id\Http\Middleware\CreateFreshApiToken::class,
]);
})
```
### Implementing Billable
To implement the `Billable` trait, you need to add the `Billable` trait to your user model.
```php
use Anikeen\Id\Billable;
class User extends Authenticatable
{
use Billable;
// Your model code...
}
```
then, you can use the `Billable` trait methods in your user model.
### Change the default access token / refresh token field name
If you access / refresh token fields differs from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
```php
use Anikeen\Id\AnikeenId;
public function boot(): void
{
AnikeenId::useAccessTokenField('anikeen_id_access_token');
AnikeenId::useRefreshTokenField('anikeen_id_refresh_token');
}
```
### Implementing Auth
This method should typically be called in the `boot` method of your `AuthServiceProvider` class: This method should typically be called in the `boot` method of your `AuthServiceProvider` class:
@@ -79,12 +116,7 @@ use Anikeen\Id\AnikeenId;
use Anikeen\Id\Providers\AnikeenIdSsoUserProvider; use Anikeen\Id\Providers\AnikeenIdSsoUserProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
/** public function boot(): void
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{ {
Auth::provider('sso-users', function ($app, array $config) { Auth::provider('sso-users', function ($app, array $config) {
return new AnikeenIdSsoUserProvider( return new AnikeenIdSsoUserProvider(
@@ -92,7 +124,6 @@ public function boot()
$app->make(Request::class), $app->make(Request::class),
$config['model'], $config['model'],
$config['fields'] ?? [], $config['fields'] ?? [],
$config['access_token_field'] ?? null
); );
}); });
} }
@@ -108,7 +139,7 @@ reference the guard in the `guards` configuration of your `auth.php` configurati
], ],
'api' => [ 'api' => [
'driver' => 'anikeen-id', 'driver' => 'anikeen',
'provider' => 'sso-users', 'provider' => 'sso-users',
], ],
], ],
@@ -127,35 +158,13 @@ reference the provider in the `providers` configuration of your `auth.php` confi
'driver' => 'sso-users', 'driver' => 'sso-users',
'model' => App\Models\User::class, 'model' => App\Models\User::class,
'fields' => ['first_name', 'last_name', 'email'], 'fields' => ['first_name', 'last_name', 'email'],
'access_token_field' => 'sso_access_token',
], ],
], ],
``` ```
## Examples ## General
#### Basic #### Setters and Getters
```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
```php ```php
$anikeenId = new Anikeen\Id\AnikeenId(); $anikeenId = new Anikeen\Id\AnikeenId();
@@ -169,6 +178,72 @@ $anikeenId = $anikeenId->withClientSecret('abc123');
$anikeenId = $anikeenId->withToken('abcdef123456'); $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 #### OAuth Tokens
```php ```php
@@ -202,52 +277,120 @@ AnikeenId::withClientId('abc123')->withToken('abcdef123456')->getAuthedUser();
## Documentation ## Documentation
## AnikeenId
### Oauth ### Oauth
```php ```php
public function retrievingToken(string $grantType, array $attributes) public function retrievingToken(string $grantType, array $attributes): Result
``` ```
### SshKeys ### ManagesPricing
```php ```php
public function getSshKeysByUserId(int $id) public function createOrderPreview(array $attributes = []): Result
public function createSshKey(string $publicKey, string $name = NULL)
public function deleteSshKey(int $id)
``` ```
### Users ### ManagesSshKeys
```php ```php
public function getAuthedUser() public function sshKeysByUserId(string $sskKeyId): Result
public function createUser(array $parameters) public function createSshKey(string $publicKey, ?string $name = null): Result
public function isEmailExisting(string $email) public function deleteSshKey(int $sshKeyId): Result
``` ```
### Delete ### ManagesUsers
```php ```php
public function getAuthedUser(): Result
public function createUser(array $attributes): Result
public function isEmailExisting(string $email): Result
``` ```
### Get
## Billable
### ManagesAddresses
```php ```php
public function addresses(): Result
public function createAddress(array $attributes = []): Result
public function address(string $addressId): Result
public function updateAddress(string $addressId, array $attributes = []): Result
public function deleteAddress(string $addressId): Result
``` ```
### Post ### ManagesBalance
```php ```php
public function balance(): float
public function charges(): float
public function charge(float $amount, string $paymentMethodId, array $options = []): Result
``` ```
### Put ### ManagesInvoices
```php ```php
public function invoices(): Result
public function invoice(string $invoiceId): Result
public function getInvoiceTemporaryUrl(string $invoiceId): string
``` ```
### 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
public function deleteSubscription(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) [**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Enums/Scope.php)
## Development ## Development

View File

@@ -9,8 +9,8 @@ PHP Anikeen ID API Client for Laravel 11+
## Table of contents ## Table of contents
1. [Installation](#installation) 1. [Installation](#installation)
2. [Event Listener](#event-listener) 2. [Configuration](#configuration)
3. [Configuration](#configuration) 3. [General](#general)
4. [Examples](#examples) 4. [Examples](#examples)
5. [Documentation](#documentation) 5. [Documentation](#documentation)
6. [Development](#Development) 6. [Development](#Development)
@@ -21,45 +21,93 @@ PHP Anikeen ID API Client for Laravel 11+
composer require anikeen/id composer require anikeen/id
``` ```
## Event Listener
In Laravel 11, the default EventServiceProvider provider was removed. Instead, add the listener using the listen method on the Event facade, in your `AppServiceProvider`
```
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('anikeen-id', \Anikeen\Id\Socialite\Provider::class);
});
```
## Configuration ## Configuration
Copy configuration to config folder: Add environmental variables to your `.env` file:
```
$ php artisan vendor:publish --provider="Anikeen\Id\Providers\AnikeenIdServiceProvider"
```
Add environmental variables to your `.env`
``` ```
ANIKEEN_ID_KEY= ANIKEEN_ID_KEY=
ANIKEEN_ID_SECRET= ANIKEEN_ID_SECRET=
ANIKEEN_ID_REDIRECT_URI=http://localhost ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback
```
To switch from `production` to `staging` use following variable:
```
ANIKEEN_ID_MODE=staging
``` ```
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. 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.
**Add to `config/services.php`:** Add to `config/services.php` file:
```php ```php
'anikeen-id' => [ 'anikeen' => [
'mode' => env('ANIKEEN_ID_MODE'),
'client_id' => env('ANIKEEN_ID_KEY'), 'client_id' => env('ANIKEEN_ID_KEY'),
'client_secret' => env('ANIKEEN_ID_SECRET'), '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'),
], ],
``` ```
## Implementing Auth ### Event Listener
In Laravel 11, the default EventServiceProvider provider was removed. Instead, add the listener using the listen method on the Event facade, in your `AppServiceProvider` boot method:
```php
public function boot(): void
{
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('anikeen', \Anikeen\Id\Socialite\Provider::class);
});
}
```
### Registering Middleware
Append it to the global middleware stack in your application's `bootstrap/app.php` file:
```php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Anikeen\Id\Http\Middleware\CreateFreshApiToken::class,
]);
})
```
### Implementing Billable
To implement the `Billable` trait, you need to add the `Billable` trait to your user model.
```php
use Anikeen\Id\Billable;
class User extends Authenticatable
{
use Billable;
// Your model code...
}
```
then, you can use the `Billable` trait methods in your user model.
### Change the default access token / refresh token field name
If you access / refresh token fields differs from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
```php
use Anikeen\Id\AnikeenId;
public function boot(): void
{
AnikeenId::useAccessTokenField('anikeen_id_access_token');
AnikeenId::useRefreshTokenField('anikeen_id_refresh_token');
}
```
### Implementing Auth
This method should typically be called in the `boot` method of your `AuthServiceProvider` class: This method should typically be called in the `boot` method of your `AuthServiceProvider` class:
@@ -68,12 +116,7 @@ use Anikeen\Id\AnikeenId;
use Anikeen\Id\Providers\AnikeenIdSsoUserProvider; use Anikeen\Id\Providers\AnikeenIdSsoUserProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
/** public function boot(): void
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{ {
Auth::provider('sso-users', function ($app, array $config) { Auth::provider('sso-users', function ($app, array $config) {
return new AnikeenIdSsoUserProvider( return new AnikeenIdSsoUserProvider(
@@ -81,7 +124,6 @@ public function boot()
$app->make(Request::class), $app->make(Request::class),
$config['model'], $config['model'],
$config['fields'] ?? [], $config['fields'] ?? [],
$config['access_token_field'] ?? null
); );
}); });
} }
@@ -97,7 +139,7 @@ reference the guard in the `guards` configuration of your `auth.php` configurati
], ],
'api' => [ 'api' => [
'driver' => 'anikeen-id', 'driver' => 'anikeen',
'provider' => 'sso-users', 'provider' => 'sso-users',
], ],
], ],
@@ -116,35 +158,13 @@ reference the provider in the `providers` configuration of your `auth.php` confi
'driver' => 'sso-users', 'driver' => 'sso-users',
'model' => App\Models\User::class, 'model' => App\Models\User::class,
'fields' => ['first_name', 'last_name', 'email'], 'fields' => ['first_name', 'last_name', 'email'],
'access_token_field' => 'sso_access_token',
], ],
], ],
``` ```
## Examples ## General
#### Basic #### Setters and Getters
```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
```php ```php
$anikeenId = new Anikeen\Id\AnikeenId(); $anikeenId = new Anikeen\Id\AnikeenId();
@@ -158,6 +178,72 @@ $anikeenId = $anikeenId->withClientSecret('abc123');
$anikeenId = $anikeenId->withToken('abcdef123456'); $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 #### OAuth Tokens
```php ```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

@@ -1,85 +1,111 @@
<?php <?php
require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
use Anikeen\Id\AnikeenId; use Anikeen\Id\AnikeenId;
use Illuminate\Support\Arr; use Anikeen\Id\Billable;
use Illuminate\Support\Arr;
$markdown = collect(class_uses(AnikeenId::class)) use Illuminate\Support\Str;
->map(function ($trait) {
// Liste der Klassen, die ausgewertet werden sollen
$title = str_replace('Trait', '', Arr::last(explode('\\', $trait))); $classes = [
AnikeenId::class,
$methods = []; Billable::class,
];
$reflection = new ReflectionClass($trait);
$allMarkdown = collect($classes)
collect($reflection->getMethods()) ->map(function (string $class) {
->reject(function (ReflectionMethod $method) { $className = Arr::last(explode('\\', $class));
return $method->isAbstract(); $markdown = "## {$className}\n\n";
})
->reject(function (ReflectionMethod $method) { // alle Traits der Klasse, außer denen aus ApiOperations
return $method->isPrivate() || $method->isProtected(); $traits = collect(class_uses($class) ?: [])
}) ->reject(function (string $trait) {
->reject(function (ReflectionMethod $method) { return Str::contains($trait, 'ApiOperations\\');
return $method->isConstructor(); })
}) ->all();
->each(function (ReflectionMethod $method) use (&$methods, $title, $trait) {
if (empty($traits)) {
$declaration = collect($method->getModifiers())->map(function (int $modifier) { $markdown .= '_Keine Traits gefunden._';
return $modifier == ReflectionMethod::IS_PUBLIC ? 'public ' : ''; return $markdown;
})->join(' '); }
$declaration .= 'function '; // für jeden Trait die Methoden extrahieren
$declaration .= $method->getName(); $markdown .= collect($traits)
$declaration .= '('; ->map(function (string $trait) {
$title = str_replace('Trait', '', Arr::last(explode('\\', $trait)));
$declaration .= collect($method->getParameters())->map(function (ReflectionParameter $parameter) { $reflection = new ReflectionClass($trait);
$parameterString = Arr::last(explode('\\', $parameter->getType()->getName())); $methods = collect($reflection->getMethods())
$parameterString .= ' '; ->reject->isAbstract()
$parameterString .= '$'; ->reject->isPrivate()
$parameterString .= $parameter->getName(); ->reject->isProtected()
->reject->isConstructor()
if ($parameter->isDefaultValueAvailable()) { ->map(function (ReflectionMethod $method) {
$parameterString .= ' = '; // Methodendeklaration starten
$parameterString .= str_replace(PHP_EOL, '', var_export($parameter->getDefaultValue(), true)); $decl = 'public function ' . $method->getName() . '(';
}
// Parameter-Typen und Default-Werte
return $parameterString; $decl .= collect($method->getParameters())
->map(function (ReflectionParameter $p) {
})->join(', '); // Typ-Hint
$typeHint = '';
$declaration .= ')'; if ($p->hasType()) {
$type = $p->getType();
$methods[] = $declaration; $nullable = $type->allowsNull() ? '?' : '';
}); $name = Arr::last(explode('\\', $type->getName()));
$typeHint = $nullable . $name . ' ';
return [$title, $methods]; }
})
->map(function ($args) { // Parameter-Name
$param = $typeHint . '$' . $p->getName();
list($title, $methods) = $args;
// Default-Wert
$markdown = '### ' . $title; if ($p->isDefaultValueAvailable()) {
$markdown .= PHP_EOL . PHP_EOL; $default = $p->getDefaultValue();
$markdown .= '```php'; if (is_array($default) && empty($default)) {
$markdown .= PHP_EOL; // leeres Array → Short-Syntax
$param .= ' = []';
$markdown .= collect($methods)->each(function ($method) { } elseif ($default === null) {
return $method; // NULL → null (kleingeschrieben)
})->implode(PHP_EOL); $param .= ' = null';
} else {
$markdown .= PHP_EOL; // sonst var_export, Newlines entfernen
$markdown .= '```'; $def = var_export($default, true);
$param .= ' = ' . str_replace(PHP_EOL, '', $def);
return $markdown; }
})->join(PHP_EOL . PHP_EOL); }
$markdown = str_replace("array (\n)", '[]', $markdown); return $param;
})
$content = file_get_contents(__DIR__ . '/../README.stub'); ->implode(', ');
$content = str_replace('<!-- GENERATED-DOCS -->', $markdown, $content); $decl .= ')';
file_put_contents(__DIR__ . '/../README.md', $content); // 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('<!-- GENERATED-DOCS -->', $allMarkdown, $stub);
file_put_contents(__DIR__ . '/../README.md', $content);

View File

@@ -23,7 +23,7 @@
<php> <php>
<env name="ANIKEEN_ID_KEY" value="38"/> <env name="ANIKEEN_ID_KEY" value="38"/>
<env name="ANIKEEN_ID_SECRET" value="2goNRF8x37HPVZVaa28ySZGVXJuksvxnxM7TcGzM"/> <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"/> <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> </php>
</phpunit> </phpunit>

View File

@@ -2,6 +2,9 @@
namespace Anikeen\Id; 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\RequestRequiresAuthenticationException;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException; use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Exceptions\RequestRequiresRedirectUriException; use Anikeen\Id\Exceptions\RequestRequiresRedirectUriException;
@@ -15,14 +18,16 @@ use Illuminate\Contracts\Auth\Authenticatable;
class AnikeenId class AnikeenId
{ {
use Traits\OauthTrait; use OauthTrait;
use Traits\SshKeysTrait; use ManagesPricing;
use Traits\UsersTrait; use ManagesSshKeys;
use ManagesUsers;
use ApiOperations\Delete; use ApiOperations\Delete;
use ApiOperations\Get; use ApiOperations\Get;
use ApiOperations\Post; use ApiOperations\Post;
use ApiOperations\Put; use ApiOperations\Put;
use ApiOperations\Request;
/** /**
* The name for API token cookies. * The name for API token cookies.
@@ -41,8 +46,26 @@ class AnikeenId
*/ */
public static bool $unserializesCookies = false; public static bool $unserializesCookies = false;
/**
* The base URL for Anikeen ID API.
*/
private static string $baseUrl = 'https://id.anikeen.com/api/'; private static string $baseUrl = 'https://id.anikeen.com/api/';
/**
* The staging base URL for Anikeen ID API.
*/
private static string $stagingBaseUrl = 'https://staging.id.anikeen.com/api/';
/**
* The key for the access token.
*/
private static string $accessTokenField = 'anikeen_id_access_token';
/**
* The key for the access token.
*/
private static string $refreshTokenField = 'anikeen_id_refresh_token';
/** /**
* Guzzle is used to make http requests. * Guzzle is used to make http requests.
*/ */
@@ -55,13 +78,11 @@ class AnikeenId
/** /**
* Anikeen ID OAuth token. * Anikeen ID OAuth token.
*
*/ */
protected ?string $token = null; protected ?string $token = null;
/** /**
* Anikeen ID client id. * Anikeen ID client id.
*
*/ */
protected ?string $clientId = null; protected ?string $clientId = null;
@@ -80,17 +101,20 @@ class AnikeenId
*/ */
public function __construct() public function __construct()
{ {
if ($clientId = config('anikeen_id.client_id')) { if ($clientId = config('services.anikeen.client_id')) {
$this->setClientId($clientId); $this->setClientId($clientId);
} }
if ($clientSecret = config('anikeen_id.client_secret')) { if ($clientSecret = config('services.anikeen.client_secret')) {
$this->setClientSecret($clientSecret); $this->setClientSecret($clientSecret);
} }
if ($redirectUri = config('anikeen_id.redirect_url')) { if ($redirectUri = config('services.anikeen.redirect')) {
$this->setRedirectUri($redirectUri); $this->setRedirectUri($redirectUri);
} }
if ($redirectUri = config('anikeen_id.base_url')) { if (config('services.anikeen.mode') === 'staging') {
self::setBaseUrl($redirectUri); self::setBaseUrl(self::$stagingBaseUrl);
}
if ($baseUrl = config('services.anikeen.base_url')) {
self::setBaseUrl($baseUrl);
} }
$this->client = new Client([ $this->client = new Client([
'base_uri' => self::$baseUrl, 'base_uri' => self::$baseUrl,
@@ -107,13 +131,33 @@ class AnikeenId
self::$baseUrl = $baseUrl; self::$baseUrl = $baseUrl;
} }
public static function useAccessTokenField(string $accessTokenField): void
{
self::$accessTokenField = $accessTokenField;
}
public static function getAccessTokenField(): string
{
return self::$accessTokenField;
}
public static function useRefreshTokenField(string $refreshTokenField): void
{
self::$refreshTokenField = $refreshTokenField;
}
public static function getRefreshTokenField(): string
{
return self::$refreshTokenField;
}
/** /**
* Get or set the name for API token cookies. * Get or set the name for API token cookies.
* *
* @param string|null $cookie * @param string|null $cookie
* @return string|static * @return string|static
*/ */
public static function cookie(string $cookie = null) public static function cookie(?string $cookie = null): string|static
{ {
if (is_null($cookie)) { if (is_null($cookie)) {
return static::$cookie; return static::$cookie;
@@ -127,7 +171,7 @@ class AnikeenId
/** /**
* Set the current user for the application with the given scopes. * 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)[ $user->withAnikeenAccessToken((object)[
'scopes' => $scopes 'scopes' => $scopes
@@ -251,12 +295,25 @@ class AnikeenId
} }
/** /**
* @throws GuzzleException * Get client id.
*
* @throws RequestRequiresClientIdException * @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 +322,23 @@ class AnikeenId
* @throws GuzzleException * @throws GuzzleException
* @throws RequestRequiresClientIdException * @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) { if ($paginator !== null) {
$parameters[$paginator->action] = $paginator->cursor(); $parameters[$paginator->action] = $paginator->cursor();
} }
try { try {
$response = $this->client->request($method, $path, [ $response = $this->client->request($method, $path, [
'headers' => $this->buildHeaders((bool)$jsonBody), 'headers' => $this->buildHeaders((bool)$payload),
'query' => Query::build($parameters), '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) { } catch (RequestException $exception) {
$result = new Result($exception->getResponse(), $exception, $paginator); $result = new Result($exception->getResponse(), $exception, $this);
} }
$result->anikeenId = $this;
return $result; return $result;
} }
@@ -308,64 +365,38 @@ class AnikeenId
} }
/** /**
* Get client id. * @throws GuzzleException
*
* @throws RequestRequiresClientIdException * @throws RequestRequiresClientIdException
*/ */
public function getClientId(): string public function get(string $path, array $parameters = [], ?Paginator $paginator = null): Result
{ {
if (!$this->clientId) { return $this->request('GET', $path, null, $parameters, $paginator);
throw new RequestRequiresClientIdException;
}
return $this->clientId;
}
/**
* Set client id.
*/
public function setClientId(string $clientId): void
{
$this->clientId = $clientId;
} }
/** /**
* @throws GuzzleException * @throws GuzzleException
* @throws RequestRequiresClientIdException * @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 GuzzleException
* @throws RequestRequiresClientIdException * @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 GuzzleException
* @throws RequestRequiresClientIdException * @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); return $this->request('DELETE', $path, $payload, $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);
} }
} }

View File

@@ -2,10 +2,18 @@
namespace Anikeen\Id\ApiOperations; namespace Anikeen\Id\ApiOperations;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Helpers\Paginator;
use Anikeen\Id\Result; use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait Delete 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; namespace Anikeen\Id\ApiOperations;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Helpers\Paginator;
use Anikeen\Id\Result; use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait Get 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; namespace Anikeen\Id\ApiOperations;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Helpers\Paginator;
use Anikeen\Id\Result; use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait Post 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; namespace Anikeen\Id\ApiOperations;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Helpers\Paginator; use Anikeen\Id\Helpers\Paginator;
use Anikeen\Id\Result; use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait Put 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 * @throws RequestRequiresMissingParametersException
*/ */
public function validateRequired(array $parameters, array $required) public function validateRequired(array $parameters, array $required): void
{ {
if (!Arr::has($parameters, $required)) { if (!Arr::has($parameters, $required)) {
throw RequestRequiresMissingParametersException::fromValidateRequired($parameters, $required); throw RequestRequiresMissingParametersException::fromValidateRequired($parameters, $required);

View File

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

View File

@@ -3,8 +3,8 @@
namespace Anikeen\Id\Auth; namespace Anikeen\Id\Auth;
use Anikeen\Id\AnikeenId; use Anikeen\Id\AnikeenId;
use Anikeen\Id\HasAnikeenTokens;
use Anikeen\Id\Helpers\JwtParser; use Anikeen\Id\Helpers\JwtParser;
use Anikeen\Id\Traits\HasAnikeenTokens;
use Exception; use Exception;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use Firebase\JWT\Key; use Firebase\JWT\Key;

View File

@@ -62,4 +62,9 @@ class UserProvider implements Base
{ {
return $this->providerName; return $this->providerName;
} }
public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false)
{
// TODO: Implement rehashPasswordIfRequired() method.
}
} }

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

@@ -0,0 +1,64 @@
<?php
namespace Anikeen\Id;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Concerns\ManagesAddresses;
use Anikeen\Id\Concerns\ManagesBalance;
use Anikeen\Id\Concerns\ManagesCountries;
use Anikeen\Id\Concerns\ManagesInvoices;
use Anikeen\Id\Concerns\ManagesOrders;
use Anikeen\Id\Concerns\ManagesPaymentMethods;
use Anikeen\Id\Concerns\ManagesProfile;
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 ManagesAddresses;
use ManagesBalance;
use ManagesCountries;
use ManagesInvoices;
use ManagesOrders;
use ManagesPaymentMethods;
use ManagesProfile;
use ManagesSubscriptions;
use ManagesTaxation;
use ManagesTransactions;
use Request;
protected stdClass|null $userData = null;
/**
* Get the currently authenticated user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public 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
*/
public function request(string $method, string $path, null|array $payload = null, array $parameters = [], ?Paginator $paginator = null): Result
{
$anikeenId = new AnikeenId();
$anikeenId->withToken($this->{AnikeenId::getAccessTokenField()});
return $anikeenId->request($method, $path, $payload, $parameters, $paginator);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Anikeen\Id\Concerns;
trait HasBillable
{
public mixed $billable;
public function setBillable(mixed $billable): self
{
$this->billable = $billable;
return $this;
}
public function getBillable(): mixed
{
return $this->billable;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Anikeen\Id\Concerns;
trait HasParent
{
protected mixed $parent;
public function setParent(mixed $parent): self
{
$this->parent = $parent;
return $this;
}
public function getParent()
{
return $this->parent;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Anikeen\Id\Concerns;
use stdClass;
trait MagicProperties
{
protected function setMagicProperties(stdClass $data): void
{
foreach ($data as $key => $value) {
if (!property_exists($this, $key)) {
$this->{$key} = $value;
}
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\Addresses;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesAddresses
{
use Request;
/**
* Get addresses from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function addresses(): Addresses
{
return (new Addresses($this->request('GET', 'v1/addresses')))
->setBillable($this);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\Transaction;
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 = []): Transaction
{
return new Transaction($this->request('POST', 'billing/charge', [
'amount' => $amount,
'payment_method_id' => $paymentMethodId,
'options' => $options,
]));
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\Countries;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesCountries
{
use Request;
/**
* Get available countries for the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function countries(): Countries
{
return (new Countries($this->request('GET', 'v1/countries')))
->setBillable($this);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\Invoices;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesInvoices
{
use Request;
/**
* Get invoices from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function invoices(): Invoices
{
return (new Invoices($this->request('GET', 'v1/invoices')))
->setBillable($this);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\Orders;
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(): Orders
{
return (new Orders($this->request('GET', 'v1/orders')))
->setBillable($this);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\PaymentMethods;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesPaymentMethods
{
use Request;
/**
* Get payment methods from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function paymentMethods(): PaymentMethods
{
return (new PaymentMethods($this->request('GET', 'v1/payment-methods')))
->setBillable($this);
}
/**
* Get billing portal URL for the current user.
*
* @param string|null $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 = null, array $options = []): string
{
return $this->request('POST', 'v1/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,30 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesProfile
{
use Request;
/**
* Get the profile url for the current user.
*
* @param string|null $returnUrl The URL to redirect to after the user has completed their profile.
* @param array $options Additional options for the profile URL.
* @return string
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function profilePortalUrl(?string $returnUrl = null, array $options = []): string
{
return $this->request('POST', 'v1/user/profile', [
'return_url' => $returnUrl,
'options' => $options,
])->data->url;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Get;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\SshKeys;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesSshKeys
{
use Get;
/**
* Get currently authed user with Bearer Token.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function sshKeysByUserId(string $sskKeyId): SshKeys
{
return (new SshKeys($this->get(sprintf('v1/users/%s/ssh-keys/json', $sskKeyId))))
->setParent($this);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\Subscriptions;
use GuzzleHttp\Exception\GuzzleException;
trait ManagesSubscriptions
{
use Request;
/**
* Get subscriptions from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function subscriptions(): Subscriptions
{
return (new Subscriptions($this->request('GET', 'v1/subscriptions')))
->setBillable($this);
}
}

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,26 @@
<?php
namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Request;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Resources\Transactions;
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(): Transactions
{
return (new Transactions($this->request('GET', 'v1/transactions')))
->setBillable($this);;
}
}

View File

@@ -0,0 +1,64 @@
<?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;
use HasParent;
/**
* 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 <?php
namespace Anikeen\Id\Traits; namespace Anikeen\Id;
use stdClass; use stdClass;
@@ -9,7 +9,7 @@ trait HasAnikeenTokens
/** /**
* The current access token for the authentication user. * 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. * Get the current access token being used by the user.

View File

@@ -7,70 +7,120 @@ use stdClass;
class Paginator class Paginator
{ {
/** /**
* Next desired action (first, after, before). * Next desired action: 'first', 'after', 'before'.
*
* @var string|null
*/ */
public ?string $action = 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. * 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 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 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 public function first(): self
{ {
$this->action = 'first'; $this->action = 'first';
return $this; return $this;
} }
/** /**
* Set the Paginator to fetch the first set of results. * Fetch the next page (after).
*/ */
public function next(): self public function next(): self
{ {
$this->action = 'after'; $this->action = 'after';
return $this; return $this;
} }
/** /**
* Set the Paginator to fetch the last set of results. * Fetch the previous page (before).
*/ */
public function back(): self public function back(): self
{ {
$this->action = 'before'; $this->action = 'before';
return $this; return $this;
} }
} }

View File

@@ -2,35 +2,62 @@
namespace Anikeen\Id\Http\Middleware; namespace Anikeen\Id\Http\Middleware;
use Anikeen\Id\AnikeenId;
use Anikeen\Id\ApiTokenCookieFactory; use Anikeen\Id\ApiTokenCookieFactory;
use Anikeen\Id\Facades\AnikeenId;
use Closure; use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
class CreateFreshApiToken class CreateFreshApiToken
{ {
/**
* The API token cookie factory instance.
*
* @var ApiTokenCookieFactory
*/
protected $cookieFactory;
/** /**
* The authentication guard. * The authentication guard.
* *
* @var string * @var string
*/ */
protected string $guard; protected $guard;
/** /**
* Create a new middleware instance. * Create a new middleware instance.
* *
* @param ApiTokenCookieFactory $cookieFactory
* @return void * @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. * 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; $this->guard = $guard;
@@ -47,8 +74,12 @@ class CreateFreshApiToken
/** /**
* Determine if the given request should receive a fresh token. * 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) && return $this->requestShouldReceiveFreshToken($request) &&
$this->responseShouldReceiveFreshToken($response); $this->responseShouldReceiveFreshToken($response);
@@ -56,25 +87,37 @@ class CreateFreshApiToken
/** /**
* Determine if the request should receive a fresh token. * 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); return $request->isMethod('GET') && $request->user($this->guard);
} }
/** /**
* Determine if the response should receive a fresh token. * 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. * Determine if the given response already contains an API token.
*
* This avoids us overwriting a just "refreshed" 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) { foreach ($response->headers->getCookies() as $cookie) {
if ($cookie->getName() === AnikeenId::cookie()) { if ($cookie->getName() === AnikeenId::cookie()) {
@@ -84,4 +127,4 @@ class CreateFreshApiToken
return false; return false;
} }
} }

View File

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

View File

@@ -21,9 +21,7 @@ class AnikeenIdServiceProvider extends ServiceProvider
*/ */
public function boot() 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 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(Contracts\AppTokenRepository::class, Repository\AppTokenRepository::class);
$this->app->singleton(AnikeenId::class, function () { $this->app->singleton(AnikeenId::class, function () {
return new AnikeenId; return new AnikeenId;
@@ -46,7 +43,7 @@ class AnikeenIdServiceProvider extends ServiceProvider
protected function registerGuard(): void protected function registerGuard(): void
{ {
Auth::resolved(function ($auth) { Auth::resolved(function ($auth) {
$auth->extend('anikeen-id', function ($app, $name, array $config) { $auth->extend('anikeen', function ($app, $name, array $config) {
return tap($this->makeGuard($config), function ($guard) { return tap($this->makeGuard($config), function ($guard) {
$this->app->refresh('request', $guard, 'setRequest'); $this->app->refresh('request', $guard, 'setRequest');
}); });

View File

@@ -13,28 +13,19 @@ use Illuminate\Support\Arr;
class AnikeenIdSsoUserProvider implements UserProvider class AnikeenIdSsoUserProvider implements UserProvider
{ {
private AnikeenId $anikeenId;
private ?string $accessTokenField = null; private ?string $accessTokenField = null;
private array $fields;
private string $model;
private Request $request;
public function __construct( public function __construct(
AnikeenId $anikeenId, private AnikeenId $anikeenId,
Request $request, private Request $request,
string $model, private string $model,
array $fields, private array $fields
?string $accessTokenField = null
) )
{ {
$this->request = $request; $this->accessTokenField = AnikeenId::getAccessTokenField();
$this->model = $model;
$this->fields = $fields;
$this->accessTokenField = $accessTokenField;
$this->anikeenId = $anikeenId;
} }
public function retrieveById(mixed $identifier): Builder|Model|null public function retrieveById(mixed $identifier): ?Authenticatable
{ {
$model = $this->createModel(); $model = $this->createModel();
$token = $this->request->bearerToken(); $token = $this->request->bearerToken();
@@ -114,4 +105,9 @@ class AnikeenIdSsoUserProvider implements UserProvider
{ {
return false; return false;
} }
public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false)
{
// TODO: Implement rehashPasswordIfRequired() method.
}
} }

View File

@@ -0,0 +1,85 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
/**
* @property string $id
* @property null|string $company_name
* @property null|string $first_name
* @property null|string $last_name
* @property null|string $address_2
* @property null|string $address
* @property null|string $house_number
* @property null|string $postal_code
* @property null|string $city
* @property null|string $state
* @property string $country_iso
* @property null|string $phone_number
* @property null|string $email
* @property bool $primary
* @property bool $primary_billing_address
*/
class Address extends BaseResource
{
use HasBillable;
/**
* Update given address from the current user.
*
* VAT is calculated based on the billing address and shown in the address response.
*
* @param array{
* company_name: null|string,
* first_name: string,
* last_name: string,
* address_2: null|string,
* address: string,
* house_number: null|string,
* postal_code: string,
* city: string,
* state: null|string,
* country_iso: string,
* phone_number: null|string,
* email: null|string,
* primary: bool,
* primary_billing_address: bool
* } $attributes The address data:
* - company_name: Company name (optional)
* - first_name: First name (required when set)
* - last_name: Last name (required when set)
* - address: Address line 1 (e.g. street address, P.O. Box, etc.)
* - address_2: Address line 2 (optional, e.g. apartment number, c/o, etc.)
* - house_number: House number (optional)
* - postal_code: Postal code (required when set)
* - city: City (required when set)
* - state: State (optional, e.g. province, region, etc.)
* - country_iso: Country ISO code (required when set, e.g. US, CA, etc.)
* - phone_number: Phone number (optional)
* - email: Email address (optional, e.g. for delivery notifications)
* - primary: Whether this address should be the primary address (optional)
* - primary_billing_address: Whether this address should be the primary billing address (optional)
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function update(array $attributes = []): self
{
return (new self($this->billable->request('PUT', sprintf('v1/addresses/%s', $this->id), $attributes)))
->setBillable($this->billable);
}
/**
* Delete given address from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function delete(): bool
{
return $this->billable->request('DELETE', sprintf('v1/addresses/%s', $this->id))->success();
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class Addresses extends BaseCollection
{
use HasBillable;
/**
* Creates a new address for the current user.
*
* @param array{
* company_name: null|string,
* first_name: string,
* last_name: string,
* address: string,
* address_2: null|string,
* house_number: null|string,
* postal_code: string,
* city: string,
* state: null|string,
* country_iso: string,
* phone_number: null|string,
* email: null|string,
* primary: bool,
* primary_billing_address: bool
* } $attributes The address data:
*   - company_name: Company name (optional)
*   - first_name: First name
*   - last_name: Last name
*   - address: Address line 1 (e.g. street address, P.O. Box, etc.)
*   - address_2: Address line 2 (optional, e.g. apartment number, c/o, etc.)
*   - house_number: House number (optional)
*   - postal_code: Postal code
*   - city: City
*   - state: State (optional, e.g. province, region, etc.)
*   - country_iso: Country ISO code (e.g. US, CA, etc.)
*   - phone_number: Phone number (optional)
*   - email: Email address (optional, e.g. for delivery notifications)
*   - primary: Whether this address should be the primary address (optional)
*   - primary_billing_address: Whether this address should be the primary billing address (optional)
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function create(array $attributes = []): Address
{
return (new Address($this->billable->request('POST', 'v1/addresses', $attributes)))
->setBillable($this->billable);
}
/**
* {@inheritDoc}
*/
public function find(string $id): ?Address
{
$result = $this->billable->request('GET', sprintf('v1/addresses/%s', $id));
return $result->success() ?
(new Address($result))
->setBillable($this->billable)
: null;
}
/**
* Get default address from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function defaultBillingAddress(): Address
{
return (new Address($this->billable->request('GET', sprintf('v1/addresses/%s', $this->billable->getUserData()->billing_address_id))))
->setBillable($this->billable);
}
/**
* Check if the current user has a default billing address.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function hasDefaultBillingAddress(): bool
{
return $this->billable->getUserData()->billing_address_id !== null;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Result;
use JsonSerializable;
abstract class BaseCollection implements JsonSerializable
{
public function __construct(protected Result $result)
{
//
}
/**
* Returns the collection of resources as an array.
*/
public function toArray(): array
{
return (array)$this->result->data;
}
/**
* Returns the collection of resources as a JSON string.
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* Returns the collection of resources.
*/
public function paginate(): Result
{
return $this->result;
}
/**
* Returns the Resource based on the ID.
*/
abstract public function find(string $id): ?BaseResource;
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\MagicProperties;
use Anikeen\Id\Result;
abstract class BaseResource
{
use MagicProperties;
public function __construct(protected Result $result)
{
$this->setMagicProperties($this->result->data);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
class Countries extends BaseCollection
{
use HasBillable;
/**
* {@inheritDoc}
*/
public function find(string $id): ?BaseResource
{
return null;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use GuzzleHttp\Exception\GuzzleException;
/**
* @property string $id
*/
class Invoice extends BaseResource
{
use HasBillable;
/**
* Get temporary download url from given invoice.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function getInvoiceTemporaryUrl(): string
{
return $this->billable->request('PUT', sprintf('v1/invoices/%s', $this->id))->data->temporary_url;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class Invoices extends BaseCollection
{
use HasBillable;
/**
* {@inheritDoc}
*/
public function find(string $id): ?Invoice
{
$result = $this->billable->request('GET', sprintf('v1/invoices/%s', $id));
return $result->success()
? (new Invoice($result))
->setBillable($this->billable)
: null;
}
}

115
src/Id/Resources/Order.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use GuzzleHttp\Exception\GuzzleException;
/**
* @property string $id
*/
class Order extends BaseResource
{
use HasBillable;
/**
* Update given order from the current user.
*
* VAT is calculated based on the billing address and shown in the order response.
*
* The billing and shipping addresses are each persisted as standalone Address entities
* in the database, but are also embedded (deep-copied) into the Order object itself
* rather than merely referenced. This guarantees that the order retains its own snapshot
* of both addresses for future reference.
*
* @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 update(array $attributes = []): self
{
return (new self($this->billable->request('PUT', sprintf('v1/orders/%s', $this->id), $attributes)))
->setBillable($this->billable);
}
/**
* Checkout given order from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function checkout(string $orderId): self
{
return (new self($this->billable->request('PUT', sprintf('v1/orders/%s/checkout', $this->id))))
->setBillable($this->billable);
}
/**
* Revoke given order from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function revoke(string $orderId): self
{
return (new self($this->billable->request('PUT', sprintf('v1/orders/%s/revoke', $this->id))))
->setBillable($this->billable);
}
/**
* Delete given order from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function delete(): bool
{
return $this->billable->request('DELETE', sprintf('v1/orders/%s', $this->id))->success();
}
/**
* Get order items from given order.
*
* @param string $orderId The order ID.
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function orderItems(string $orderId): OrderItems
{
return (new OrderItems($this->billable->request('GET', sprintf('v1/orders/%s/items', $this->id))))
->setBillable($this->billable)
->setParent($this);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Concerns\HasParent;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use GuzzleHttp\Exception\GuzzleException;
/**
* @property string $id
*/
class OrderItem extends BaseResource
{
use HasBillable;
use HasParent;
/**
* Update given order item from given order.
*
* VAT is calculated based on the billing address and shown in the order item response.
*
* @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 update(array $attributes = []): self
{
return (new self($this->billable->request('PUT', sprintf('v1/orders/%s/items/%s', $this->parent->id, $this->id), $attributes)))
->setBillable($this->billable)
->setParent($this->parent);
}
/**
* Delete given order item from given order.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function delete(): bool
{
return $this->billable->request('DELETE', sprintf('v1/orders/%s/items/%s', $this->parent->id, $this->id))->success();
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Concerns\HasParent;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class OrderItems extends BaseCollection
{
use HasBillable;
use HasParent;
/**
* 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 create(string $orderId, array $attributes = []): OrderItem
{
return (new OrderItem($this->billable->request('POST', sprintf('v1/orders/%s', $orderId), $attributes)))
->setBillable($this->billable)
->setParent($this->parent);
}
/**
* {@inheritDoc}
*/
public function find(string $id): ?OrderItem
{
/** @var Result $result */
$result = $this->parent->request('GET', sprintf('v1/orders/%s/items/%s', $this->parent->id, $id));
return $result->success() ?
(new OrderItem($result))
->setBillable($this->billable)
->setParent($this->parent)
: null;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class Orders extends BaseCollection
{
use HasBillable;
/**
* Creates a new order for the current user.
*
* VAT is calculated based on the billing address and shown in the order response.
*
* The billing and shipping addresses are each persisted as standalone Address entities
* in the database, but are also embedded (deep-copied) into the Order object itself
* rather than merely referenced. This guarantees that the order retains its own snapshot
* of both addresses for future reference.
*
* @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 create(array $attributes = []): Order
{
return (new Order($this->billable->request('POST', 'v1/orders', $attributes)))
->setBillable($this->billable);
}
/**
* Get given order from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function find(string $id): ?Order
{
/** @var Result $result */
$result = $this->billable->request('GET', sprintf('v1/orders/%s', $id));
return $result->success()
? (new Order($result))
->setBillable($this->billable)
: null;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
class PaymentMethod extends BaseResource
{
use HasBillable;
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class PaymentMethods extends BaseCollection
{
use HasBillable;
/**
* Check if current user has at least one payment method.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function hasPaymentMethod(): bool
{
return $this->result->count() > 0;
}
/**
* Get default payment method from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function defaultPaymentMethod(): PaymentMethod
{
return (new PaymentMethod($this->billable->request('GET', 'v1/payment-methods/default')))
->setBillable($this->billable);
}
/**
* {@inheritDoc}
*/
public function find(string $id): ?PaymentMethod
{
$result = $this->billable->request('GET', sprintf('v1/payment-methods/%s', $id));
return $result->success()
? (new PaymentMethod($result))
->setBillable($this->billable)
: null;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasParent;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use GuzzleHttp\Exception\GuzzleException;
/**
* @property string $id
*/
class SshKey extends BaseResource
{
use HasParent;
/**
* Deletes a given ssh key for the currently authed user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function delete(): bool
{
return $this->parent->delete(sprintf('v1/ssh-keys/%s', $this->id))->success();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasParent;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class SshKeys extends BaseCollection
{
use HasParent;
/**
* 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 create(string $publicKey, ?string $name = null): SshKey
{
return (new SshKey($this->parent->post('v1/ssh-keys', [
'public_key' => $publicKey,
'name' => $name,
])))->setParent($this->parent);
}
/**
* {@inheritDoc}
*/
public function find(string $id): ?SshKey
{
/** @var Result $result */
$result = $this->parent->get(sprintf('v1/ssh-keys/%s', $id));
return $result->success()
? (new SshKey($result))
->setParent($this->parent)
: null;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use GuzzleHttp\Exception\GuzzleException;
/**
* @property string $id
*/
class Subscription extends BaseResource
{
use HasBillable;
/**
* Force given subscription to check out (trusted apps only).
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function checkout(): self
{
return (new self($this->billable->request('PUT', sprintf('v1/subscriptions/%s/checkout', $this->id))))
->setBillable($this->billable);
}
/**
* Revoke a given running subscription from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function revoke(): self
{
return (new self($this->billable->request('PUT', sprintf('v1/subscriptions/%s/revoke', $this->id))))
->setBillable($this->billable);
}
/**
* Resume a given running subscription from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function resume(): self
{
return (new self($this->billable->request('PUT', sprintf('v1/subscriptions/%s/resume', $this->id))))
->setBillable($this->billable);
}
/**
* Delete a given subscription from the current user.
*
* @throws RequestRequiresClientIdException
* @throws GuzzleException
*/
public function delete(): bool
{
return $this->billable->request('DELETE', sprintf('v1/subscriptions/%s', $this->id))->success();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class Subscriptions extends BaseCollection
{
use HasBillable;
/**
* 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 create(array $attributes): Subscription
{
return (new Subscription($this->billable->request('POST', 'v1/subscriptions', $attributes)))
->setBillable($this->billable);
}
/**
* {@inheritDoc}
*/
public function find(string $id): ?Subscription
{
$result = $this->billable->request('GET', sprintf('v1/subscriptions/%s', $id));
return $result->success()
? (new Subscription($result))
->setBillable($this->billable)
: null;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
class Transaction extends BaseResource
{
use HasBillable;
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Anikeen\Id\Resources;
use Anikeen\Id\Concerns\HasBillable;
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
class Transactions extends BaseCollection
{
use HasBillable;
/**
* 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 create(array $attributes = []): Transaction
{
return new Transaction($this->billable->request('POST', 'v1/transactions', $attributes));
}
/**
* {@inheritDoc}
*/
public function find(string $id): ?Transaction
{
$result = $this->billable->request('GET', sprintf('v1/transactions/%s', $id));
return $result->success()
? (new Transaction($result))
->setBillable($this->billable)
: null;
}
}

View File

@@ -9,67 +9,112 @@ use stdClass;
class Result class Result
{ {
/** /**
* Query successful. * Was the API call successful?
*/ */
public bool $success = false; 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; public int $total = 0;
/** /**
* Status Code. * HTTP status code
*/ */
public int $status = 0; 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. * Pagination meta (current_page, last_page etc.) as stdClass or null
* */
* @var AnikeenId 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 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->success = $exception === null;
$this->status = $response ? $response->getStatusCode() : 500; $this->status = $response ? $response->getStatusCode() : 500;
$jsonResponse = $response ? @json_decode($response->getBody()->getContents(), false) : null;
if ($jsonResponse !== null) { $raw = $response ? (string) $response->getBody() : null;
$this->setProperty($jsonResponse, 'data'); $json = $raw ? @json_decode($raw, false) : null;
$this->setProperty($jsonResponse, 'total');
$this->setProperty($jsonResponse, 'pagination'); if ($json !== null) {
$this->paginator = Paginator::from($this); // 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. * Was the request successful?
*/
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.
*/ */
public function success(): bool 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 public function error(): string
{ {
// TODO Switch Exception response parsing to this->data if ($this->exception === null || !method_exists($this->exception, 'getResponse')) {
if ($this->exception === null || !$this->exception->hasResponse()) {
return 'Anikeen ID API Unavailable'; return 'Anikeen ID API Unavailable';
} }
$exception = (string)$this->exception->getResponse()->getBody(); $resp = $this->exception->getResponse();
$exception = @json_decode($exception); $body = $resp ? (string) $resp->getBody() : null;
if (property_exists($exception, 'message') && !empty($exception->message)) { $err = $body ? @json_decode($body) : null;
return $exception->message; if (isset($err->message) && $err->message !== '') {
return $err->message;
} }
return $this->exception->getMessage(); 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 public function shift(): mixed
{ {
if (!empty($this->data)) { if (is_array($this->data)) {
$data = $this->data; return array_shift($this->data);
return array_shift($data);
} }
return $this->data;
return null;
} }
/** /**
* Return the current count of items in dataset. * Count of items in data
*/ */
public function count(): int 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 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 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 public function rateLimit(?string $key = null): array|int|string|null
{ {
if (!$this->response) { if (!$this->response) {
return null; return null;
} }
$rateLimit = [ $info = [
'limit' => (int)$this->response->getHeaderLine('X-RateLimit-Limit'), 'limit' => (int) $this->response->getHeaderLine('X-RateLimit-Limit'),
'remaining' => (int)$this->response->getHeaderLine('X-RateLimit-Remaining'), 'remaining' => (int) $this->response->getHeaderLine('X-RateLimit-Remaining'),
'reset' => (int)$this->response->getHeaderLine('Retry-After'), 'reset' => (int) $this->response->getHeaderLine('Retry-After'),
]; ];
if ($key === null) { return $key ? ($info[$key] ?? null) : $info;
return $rateLimit;
}
return $rateLimit[$key];
} }
/** /**
* 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 public function insertUsers(string $identifierAttribute = 'user_id', string $insertTo = 'user'): self
{ {
$data = $this->data; if (!is_array($this->data)) {
$userIds = collect($data)->map(function ($item) use ($identifierAttribute) {
return $item->{$identifierAttribute};
})->toArray();
if (count($userIds) === 0) {
return $this; return $this;
} }
$users = collect($this->anikeenId->getUsersByIds($userIds)->data); $ids = array_map(fn($item) => $item->{$identifierAttribute} ?? null, $this->data);
$dataWithUsers = collect($data)->map(function ($item) use ($users, $identifierAttribute, $insertTo) { $ids = array_filter($ids);
$item->$insertTo = $users->where('id', $item->{$identifierAttribute})->first(); if (empty($ids)) {
return $this;
return $item; }
}); $users = $this->anikeenId->getUsersByIds($ids)->data;
$this->data = $dataWithUsers->toArray(); foreach ($this->data as &$item) {
$item->{$insertTo} = collect($users)->firstWhere('id', $item->{$identifierAttribute});
}
return $this; return $this;
} }
/** /**
* Set the Paginator to fetch the first set of results. * Fetch first page paginator
*/ */
public function first(): ?Paginator public function first(): ?Paginator
{ {
return $this->paginator?->first(); return $this->paginator?->first();
} }
/**
* Original response
*/
public function response(): ?ResponseInterface public function response(): ?ResponseInterface
{ {
return $this->response; 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; return $this->data;
} }
} }

View File

@@ -8,6 +8,6 @@ class AnikeenIdExtendSocialite
{ {
public function handle(SocialiteWasCalled $socialiteWasCalled): void public function handle(SocialiteWasCalled $socialiteWasCalled): void
{ {
$socialiteWasCalled->extendSocialite('anikeen-id', Provider::class); $socialiteWasCalled->extendSocialite('anikeen', Provider::class);
} }
} }

View File

@@ -4,6 +4,7 @@ namespace Anikeen\Id\Socialite;
use Anikeen\Id\Enums\Scope; use Anikeen\Id\Enums\Scope;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Laravel\Socialite\Two\ProviderInterface; use Laravel\Socialite\Two\ProviderInterface;
use SocialiteProviders\Manager\OAuth2\AbstractProvider; use SocialiteProviders\Manager\OAuth2\AbstractProvider;
@@ -14,7 +15,7 @@ class Provider extends AbstractProvider implements ProviderInterface
/** /**
* Unique Provider Identifier. * Unique Provider Identifier.
*/ */
const IDENTIFIER = 'ANIKEEN_ID'; const IDENTIFIER = 'ANIKEEN';
/** /**
* {@inheritdoc} * {@inheritdoc}
@@ -26,13 +27,25 @@ class Provider extends AbstractProvider implements ProviderInterface
*/ */
protected $scopeSeparator = ' '; protected $scopeSeparator = ' ';
/**
* Get the base URL for the API.
*/
protected function getBaseUrl(): string
{
$mode = config('services.anikeen.mode') ?? 'production';
return $mode === 'staging'
? 'https://staging.id.anikeen.com'
: 'https://id.anikeen.com';
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function getAuthUrl($state): string protected function getAuthUrl($state): string
{ {
return $this->buildAuthUrlFromBase( return $this->buildAuthUrlFromBase(
'https://id.anikeen.com/oauth/authorize', $state $this->getBaseUrl() . '/oauth/authorize', $state
); );
} }
@@ -41,7 +54,7 @@ class Provider extends AbstractProvider implements ProviderInterface
*/ */
protected function getTokenUrl(): string protected function getTokenUrl(): string
{ {
return 'https://id.anikeen.com/oauth/token'; return $this->getBaseUrl() . '/oauth/token';
} }
/** /**
@@ -52,7 +65,7 @@ class Provider extends AbstractProvider implements ProviderInterface
protected function getUserByToken($token) protected function getUserByToken($token)
{ {
$response = $this->getHttpClient()->get( $response = $this->getHttpClient()->get(
'https://id.anikeen.com/api/v1/user', [ $this->getBaseUrl() . '/api/v1/user', [
'headers' => [ 'headers' => [
'Accept' => 'application/json', 'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token, 'Authorization' => 'Bearer ' . $token,

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,
]);
}
}