feat: scaffold php-plug-altum with AltumClient base class
Base API client for all 4 AltumCode products (analytics, biolinks, pusher, socialproof). Shared REST conventions: Bearer auth, form-encoded POST, Admin API (/admin-api/) and User API (/api/) with CRUD helpers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
337d9658bc
2 changed files with 437 additions and 0 deletions
29
composer.json
Normal file
29
composer.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "lthn/php-plug-altum",
|
||||
"description": "AltumCode product API clients (analytics, biolinks, pusher, socialproof)",
|
||||
"type": "library",
|
||||
"license": "EUPL-1.2",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"illuminate/http": "^12.0",
|
||||
"illuminate/support": "^12.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest": "^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Core\\Plug\\Altum\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Core\\Plug\\Altum\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"replace": {
|
||||
"core/php-plug-altum": "self.version"
|
||||
}
|
||||
}
|
||||
408
src/AltumClient.php
Normal file
408
src/AltumClient.php
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Altum;
|
||||
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Base API client for AltumCode products.
|
||||
*
|
||||
* All four AltumCode products (66analytics, 66biolinks, 66pusher,
|
||||
* 66socialproof) share identical REST conventions. This client wraps
|
||||
* both the Admin API and the per-user API with consistent error
|
||||
* handling and logging.
|
||||
*
|
||||
* AltumCode quirks:
|
||||
* - POST is used for both creation and updates (never PUT/PATCH).
|
||||
* - POST bodies are form-encoded (not JSON).
|
||||
* - Admin endpoints live under /admin-api/.
|
||||
* - User endpoints live under /api/{resource}/.
|
||||
*/
|
||||
class AltumClient
|
||||
{
|
||||
protected string $baseUrl;
|
||||
|
||||
protected string $adminApiKey;
|
||||
|
||||
protected ?string $userApiKey;
|
||||
|
||||
protected int $timeout;
|
||||
|
||||
public function __construct(
|
||||
string $baseUrl,
|
||||
string $adminApiKey,
|
||||
?string $userApiKey = null,
|
||||
int $timeout = 15,
|
||||
) {
|
||||
$this->baseUrl = rtrim($baseUrl, '/');
|
||||
$this->adminApiKey = $adminApiKey;
|
||||
$this->userApiKey = $userApiKey;
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Immutable cloning
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return a new client instance with a different user API key.
|
||||
*
|
||||
* The original instance is not mutated.
|
||||
*/
|
||||
public function withUserKey(string $userApiKey): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->userApiKey = $userApiKey;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Admin API — Users
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a user via the Admin API.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createUser(string $name, string $email, string $password): array
|
||||
{
|
||||
return $this->adminPost('/admin-api/users', [
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing user via the Admin API.
|
||||
*
|
||||
* @param int|string $userId
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateUser(int|string $userId, array $data): array
|
||||
{
|
||||
return $this->adminPost("/admin-api/users/{$userId}", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a VIP plan to a user (plan_expiration_date far in the future).
|
||||
*
|
||||
* This is a convenience wrapper around updateUser() that sets the
|
||||
* plan_id and a distant expiry so the user effectively has a
|
||||
* permanent subscription managed by our billing layer.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function assignVipPlan(int|string $userId, int $planId = 2, string $expiry = '2056-01-01'): array
|
||||
{
|
||||
return $this->updateUser($userId, [
|
||||
'plan_id' => $planId,
|
||||
'plan_expiration_date' => $expiry,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a one-time login code for a user.
|
||||
*
|
||||
* @return array<string, mixed> Contains 'one_time_login_code' on success.
|
||||
*/
|
||||
public function oneTimeLoginCode(int|string $userId): array
|
||||
{
|
||||
return $this->adminGet("/admin-api/users/{$userId}/one-time-login-code");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a full one-time login URL for a user.
|
||||
*
|
||||
* Fetches the login code via the Admin API and returns the
|
||||
* complete redirect URL the customer's browser should visit.
|
||||
*
|
||||
* @return array<string, mixed> Contains 'url' on success, or 'error' on failure.
|
||||
*/
|
||||
public function oneTimeLoginUrl(int|string $userId, ?string $redirectTo = null): array
|
||||
{
|
||||
$result = $this->oneTimeLoginCode($userId);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$code = $result['data']['one_time_login_code']
|
||||
?? $result['one_time_login_code']
|
||||
?? null;
|
||||
|
||||
if ($code === null) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'One-time login code not found in response.',
|
||||
'response' => $result,
|
||||
];
|
||||
}
|
||||
|
||||
$url = "{$this->baseUrl}/login/one-time-login-code/{$code}";
|
||||
|
||||
if ($redirectTo !== null) {
|
||||
$url .= '?redirect=' . urlencode($redirectTo);
|
||||
}
|
||||
|
||||
return ['url' => $url];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single user from the Admin API.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getAdminUser(int|string $userId): array
|
||||
{
|
||||
return $this->adminGet("/admin-api/users/{$userId}");
|
||||
}
|
||||
|
||||
/**
|
||||
* List users from the Admin API.
|
||||
*
|
||||
* @param array<string, mixed> $query Optional query parameters (page, etc.).
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function listAdminUsers(array $query = []): array
|
||||
{
|
||||
return $this->adminGet('/admin-api/users', $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user via the Admin API.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function deleteAdminUser(int|string $userId): array
|
||||
{
|
||||
return $this->adminDelete("/admin-api/users/{$userId}");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User API — generic resource helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Retrieve the authenticated user's own profile.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function user(): array
|
||||
{
|
||||
return $this->userGet('/api/user');
|
||||
}
|
||||
|
||||
/**
|
||||
* List resources from the User API.
|
||||
*
|
||||
* @param array<string, mixed> $query Optional query parameters.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function listResource(string $resource, array $query = []): array
|
||||
{
|
||||
return $this->userGet("/api/{$resource}/", $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single resource from the User API.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getResource(string $resource, int|string $id): array
|
||||
{
|
||||
return $this->userGet("/api/{$resource}/{$id}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resource via the User API.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createResource(string $resource, array $data): array
|
||||
{
|
||||
return $this->userPost("/api/{$resource}/", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a resource via the User API.
|
||||
*
|
||||
* AltumCode uses POST (not PUT/PATCH) for updates.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateResource(string $resource, int|string $id, array $data): array
|
||||
{
|
||||
return $this->userPost("/api/{$resource}/{$id}", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a resource via the User API.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function deleteResource(string $resource, int|string $id): array
|
||||
{
|
||||
return $this->userDelete("/api/{$resource}/{$id}");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Admin HTTP transport
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Perform a GET request against the Admin API.
|
||||
*
|
||||
* @param array<string, mixed> $query
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function adminGet(string $path, array $query = []): array
|
||||
{
|
||||
return $this->request('GET', $path, $query, $this->adminApiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a POST request against the Admin API.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function adminPost(string $path, array $data = []): array
|
||||
{
|
||||
return $this->request('POST', $path, $data, $this->adminApiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a DELETE request against the Admin API.
|
||||
*
|
||||
* @param array<string, mixed> $query
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function adminDelete(string $path, array $query = []): array
|
||||
{
|
||||
return $this->request('DELETE', $path, $query, $this->adminApiKey);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User HTTP transport
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Perform a GET request against the User API.
|
||||
*
|
||||
* @param array<string, mixed> $query
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function userGet(string $path, array $query = []): array
|
||||
{
|
||||
return $this->request('GET', $path, $query, $this->requireUserKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a POST request against the User API.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function userPost(string $path, array $data = []): array
|
||||
{
|
||||
return $this->request('POST', $path, $data, $this->requireUserKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a DELETE request against the User API.
|
||||
*
|
||||
* @param array<string, mixed> $query
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function userDelete(string $path, array $query = []): array
|
||||
{
|
||||
return $this->request('DELETE', $path, $query, $this->requireUserKey());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Core HTTP
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Send an HTTP request to the AltumCode backend.
|
||||
*
|
||||
* On failure (network error or non-2xx status), a structured error
|
||||
* array is returned rather than throwing an exception. This keeps
|
||||
* the calling code simple and avoids leaking transport details.
|
||||
*
|
||||
* @param array<string, mixed> $data Query params for GET/DELETE, form fields for POST.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function request(string $method, string $path, array $data, string $apiKey): array
|
||||
{
|
||||
$url = $this->baseUrl . $path;
|
||||
|
||||
try {
|
||||
/** @var PendingRequest $pending */
|
||||
$pending = Http::timeout($this->timeout)
|
||||
->withToken($apiKey);
|
||||
|
||||
$response = match ($method) {
|
||||
'GET' => $pending->get($url, $data),
|
||||
'POST' => $pending->asForm()->post($url, $data),
|
||||
'DELETE' => $pending->delete($url, $data),
|
||||
default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"),
|
||||
};
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json() ?? [];
|
||||
}
|
||||
|
||||
Log::warning('AltumCode API request failed', [
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'error' => true,
|
||||
'status' => $response->status(),
|
||||
'message' => "HTTP {$response->status()} from AltumCode API.",
|
||||
'body' => $response->body(),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('AltumCode API request error', [
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the user API key, ensuring one has been set.
|
||||
*
|
||||
* @throws \RuntimeException If no user API key is configured.
|
||||
*/
|
||||
protected function requireUserKey(): string
|
||||
{
|
||||
if ($this->userApiKey === null || $this->userApiKey === '') {
|
||||
throw new \RuntimeException(
|
||||
'No user API key configured. Use withUserKey() to set one before calling User API methods.'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->userApiKey;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue