You've already forked wc-licensed-product-client
Add object-oriented license client library (v0.0.2)
- Add LicenseClient with PSR-3 logging and PSR-6 caching support - Add DTO classes: LicenseInfo, LicenseStatus, ActivationResult - Add LicenseState enum for license status values - Add comprehensive exception hierarchy for error handling - Add PSR dependencies (psr/log, psr/cache, psr/http-client) - Update README with usage examples - Update CHANGELOG for v0.0.2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.2] - 2026-01-22
|
||||
|
||||
### Added
|
||||
|
||||
- Object-oriented client library (`LicenseClient`, `LicenseClientInterface`)
|
||||
- DTO classes for API responses (`LicenseInfo`, `LicenseStatus`, `ActivationResult`)
|
||||
- `LicenseState` enum for license status values
|
||||
- Comprehensive exception hierarchy for error handling
|
||||
- PSR-3 logging support (optional)
|
||||
- PSR-6 caching support (optional)
|
||||
- PSR dependencies (`psr/log`, `psr/cache`, `psr/http-client`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated README with usage examples
|
||||
|
||||
## [0.0.1] - 2026-01-22
|
||||
|
||||
### Added
|
||||
|
||||
@@ -29,9 +29,16 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
||||
|
||||
No known bugs at the moment
|
||||
|
||||
### Version 0.0.2
|
||||
|
||||
- Create an object oriented client-library for the API endpoints
|
||||
- Add Logging and caching
|
||||
- Keep in mind, that the security related parts of the client MUST be obfuscatable while keeping the developer experience nice. Do not obfuscate it yet.
|
||||
|
||||
## Technical Stack
|
||||
|
||||
- **Language:** PHP 8.3.x
|
||||
- **PHP-Standards:** PSR-3, PSR-4, PSR-6, PSR-18
|
||||
- **Coding-Style:** Symfony
|
||||
- **HTTP-Client-Library:** symfony/http-client
|
||||
- **Dependency Management:** Composer
|
||||
|
||||
90
README.md
90
README.md
@@ -15,12 +15,96 @@ composer require magdev/wc-licensed-product-client
|
||||
|
||||
## Features
|
||||
|
||||
- Easy integration in licensed software packages
|
||||
- Object-oriented client library
|
||||
- PSR-3 logging support
|
||||
- PSR-6 caching support
|
||||
- PSR-18 HTTP client compatible
|
||||
- License validation against domains
|
||||
- License activation on domains
|
||||
- License status checking
|
||||
- Comprehensive exception handling
|
||||
- Built on Symfony HttpClient
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```php
|
||||
use Magdev\WcLicensedProductClient\LicenseClient;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
|
||||
$httpClient = HttpClient::create();
|
||||
$client = new LicenseClient(
|
||||
httpClient: $httpClient,
|
||||
baseUrl: 'https://your-wordpress-site.com',
|
||||
);
|
||||
|
||||
// Validate a license
|
||||
$licenseInfo = $client->validate('ABCD-1234-EFGH-5678', 'example.com');
|
||||
echo "Product ID: " . $licenseInfo->productId;
|
||||
|
||||
// Check license status
|
||||
$status = $client->status('ABCD-1234-EFGH-5678');
|
||||
echo "Status: " . $status->status->value;
|
||||
|
||||
// Activate a license
|
||||
$result = $client->activate('ABCD-1234-EFGH-5678', 'example.com');
|
||||
echo "Activated: " . ($result->success ? 'Yes' : 'No');
|
||||
```
|
||||
|
||||
### With Logging
|
||||
|
||||
```php
|
||||
use Magdev\WcLicensedProductClient\LicenseClient;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
|
||||
$client = new LicenseClient(
|
||||
httpClient: HttpClient::create(),
|
||||
baseUrl: 'https://your-wordpress-site.com',
|
||||
logger: $yourPsrLogger, // Any PSR-3 compatible logger
|
||||
);
|
||||
```
|
||||
|
||||
### With Caching
|
||||
|
||||
```php
|
||||
use Magdev\WcLicensedProductClient\LicenseClient;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
|
||||
$client = new LicenseClient(
|
||||
httpClient: HttpClient::create(),
|
||||
baseUrl: 'https://your-wordpress-site.com',
|
||||
cache: $yourPsrCache, // Any PSR-6 compatible cache
|
||||
cacheTtl: 600, // Cache TTL in seconds (default: 300)
|
||||
);
|
||||
```
|
||||
|
||||
### Exception Handling
|
||||
|
||||
```php
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseNotFoundException;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseExpiredException;
|
||||
use Magdev\WcLicensedProductClient\Exception\DomainMismatchException;
|
||||
use Magdev\WcLicensedProductClient\Exception\RateLimitExceededException;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
||||
|
||||
try {
|
||||
$licenseInfo = $client->validate($licenseKey, $domain);
|
||||
} catch (LicenseNotFoundException $e) {
|
||||
// License key does not exist
|
||||
} catch (LicenseExpiredException $e) {
|
||||
// License has expired
|
||||
} catch (DomainMismatchException $e) {
|
||||
// License is not valid for this domain
|
||||
} catch (RateLimitExceededException $e) {
|
||||
// Too many requests, retry after $e->retryAfter seconds
|
||||
} catch (LicenseException $e) {
|
||||
// Other license-related errors
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
This client interacts with the following WooCommerce Licensed Product API endpoints:
|
||||
@@ -29,10 +113,6 @@ This client interacts with the following WooCommerce Licensed Product API endpoi
|
||||
- **POST /status** - Get detailed license status information
|
||||
- **POST /activate** - Activate a license on a domain
|
||||
|
||||
## Usage
|
||||
|
||||
Coming soon in future versions.
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"psr/cache": "^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/log": "^3.0",
|
||||
"symfony/http-client": "^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
|
||||
156
composer.lock
generated
156
composer.lock
generated
@@ -4,8 +4,57 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "dabb6ce648c8b638dc4b20b8999dea1d",
|
||||
"content-hash": "e412380889a4c25cef1aa8d453216223",
|
||||
"packages": [
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/cache.git",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Cache\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for caching libraries",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"psr",
|
||||
"psr-6"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/cache/tree/3.0.0"
|
||||
},
|
||||
"time": "2021-02-03T23:26:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
"version": "2.0.2",
|
||||
@@ -59,6 +108,111 @@
|
||||
},
|
||||
"time": "2021-11-05T16:47:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-client",
|
||||
"version": "1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-client.git",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.0 || ^8.0",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP clients",
|
||||
"homepage": "https://github.com/php-fig/http-client",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-client",
|
||||
"psr",
|
||||
"psr-18"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-client"
|
||||
},
|
||||
"time": "2023-09-23T14:17:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"homepage": "https://github.com/php-fig/http-message",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||
},
|
||||
"time": "2023-04-04T09:54:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.2",
|
||||
|
||||
22
src/Dto/ActivationResult.php
Normal file
22
src/Dto/ActivationResult.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Dto;
|
||||
|
||||
final readonly class ActivationResult
|
||||
{
|
||||
public function __construct(
|
||||
public bool $success,
|
||||
public string $message,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
success: $data['success'],
|
||||
message: $data['message'],
|
||||
);
|
||||
}
|
||||
}
|
||||
34
src/Dto/LicenseInfo.php
Normal file
34
src/Dto/LicenseInfo.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Dto;
|
||||
|
||||
final readonly class LicenseInfo
|
||||
{
|
||||
public function __construct(
|
||||
public int $productId,
|
||||
public ?\DateTimeImmutable $expiresAt = null,
|
||||
public ?int $versionId = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$expiresAt = null;
|
||||
if (isset($data['expires_at']) && $data['expires_at'] !== null) {
|
||||
$expiresAt = new \DateTimeImmutable($data['expires_at']);
|
||||
}
|
||||
|
||||
return new self(
|
||||
productId: $data['product_id'],
|
||||
expiresAt: $expiresAt,
|
||||
versionId: $data['version_id'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function isLifetime(): bool
|
||||
{
|
||||
return $this->expiresAt === null;
|
||||
}
|
||||
}
|
||||
53
src/Dto/LicenseStatus.php
Normal file
53
src/Dto/LicenseStatus.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Dto;
|
||||
|
||||
enum LicenseState: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case Inactive = 'inactive';
|
||||
case Expired = 'expired';
|
||||
case Revoked = 'revoked';
|
||||
}
|
||||
|
||||
final readonly class LicenseStatus
|
||||
{
|
||||
public function __construct(
|
||||
public bool $valid,
|
||||
public LicenseState $status,
|
||||
public string $domain,
|
||||
public ?\DateTimeImmutable $expiresAt,
|
||||
public int $activationsCount,
|
||||
public int $maxActivations,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$expiresAt = null;
|
||||
if (isset($data['expires_at']) && $data['expires_at'] !== null) {
|
||||
$expiresAt = new \DateTimeImmutable($data['expires_at']);
|
||||
}
|
||||
|
||||
return new self(
|
||||
valid: $data['valid'],
|
||||
status: LicenseState::from($data['status']),
|
||||
domain: $data['domain'],
|
||||
expiresAt: $expiresAt,
|
||||
activationsCount: $data['activations_count'],
|
||||
maxActivations: $data['max_activations'],
|
||||
);
|
||||
}
|
||||
|
||||
public function isLifetime(): bool
|
||||
{
|
||||
return $this->expiresAt === null;
|
||||
}
|
||||
|
||||
public function hasAvailableActivations(): bool
|
||||
{
|
||||
return $this->activationsCount < $this->maxActivations;
|
||||
}
|
||||
}
|
||||
9
src/Exception/ActivationFailedException.php
Normal file
9
src/Exception/ActivationFailedException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class ActivationFailedException extends LicenseException
|
||||
{
|
||||
}
|
||||
9
src/Exception/DomainMismatchException.php
Normal file
9
src/Exception/DomainMismatchException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class DomainMismatchException extends LicenseException
|
||||
{
|
||||
}
|
||||
41
src/Exception/LicenseException.php
Normal file
41
src/Exception/LicenseException.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
class LicenseException extends \RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
public readonly ?string $errorCode = null,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function fromApiResponse(array $data, int $httpCode = 0): self
|
||||
{
|
||||
$errorCode = $data['error'] ?? null;
|
||||
$message = $data['message'] ?? 'Unknown license error';
|
||||
|
||||
return match ($errorCode) {
|
||||
'license_not_found' => new LicenseNotFoundException($message, $errorCode, $httpCode),
|
||||
'license_expired' => new LicenseExpiredException($message, $errorCode, $httpCode),
|
||||
'license_revoked' => new LicenseRevokedException($message, $errorCode, $httpCode),
|
||||
'license_inactive' => new LicenseInactiveException($message, $errorCode, $httpCode),
|
||||
'license_invalid' => new LicenseInvalidException($message, $errorCode, $httpCode),
|
||||
'domain_mismatch' => new DomainMismatchException($message, $errorCode, $httpCode),
|
||||
'max_activations_reached' => new MaxActivationsReachedException($message, $errorCode, $httpCode),
|
||||
'activation_failed' => new ActivationFailedException($message, $errorCode, $httpCode),
|
||||
'rate_limit_exceeded' => new RateLimitExceededException(
|
||||
$message,
|
||||
$errorCode,
|
||||
$httpCode,
|
||||
retryAfter: $data['retry_after'] ?? null,
|
||||
),
|
||||
default => new self($message, $errorCode, $httpCode),
|
||||
};
|
||||
}
|
||||
}
|
||||
9
src/Exception/LicenseExpiredException.php
Normal file
9
src/Exception/LicenseExpiredException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class LicenseExpiredException extends LicenseException
|
||||
{
|
||||
}
|
||||
9
src/Exception/LicenseInactiveException.php
Normal file
9
src/Exception/LicenseInactiveException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class LicenseInactiveException extends LicenseException
|
||||
{
|
||||
}
|
||||
9
src/Exception/LicenseInvalidException.php
Normal file
9
src/Exception/LicenseInvalidException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class LicenseInvalidException extends LicenseException
|
||||
{
|
||||
}
|
||||
9
src/Exception/LicenseNotFoundException.php
Normal file
9
src/Exception/LicenseNotFoundException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class LicenseNotFoundException extends LicenseException
|
||||
{
|
||||
}
|
||||
9
src/Exception/LicenseRevokedException.php
Normal file
9
src/Exception/LicenseRevokedException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class LicenseRevokedException extends LicenseException
|
||||
{
|
||||
}
|
||||
9
src/Exception/MaxActivationsReachedException.php
Normal file
9
src/Exception/MaxActivationsReachedException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class MaxActivationsReachedException extends LicenseException
|
||||
{
|
||||
}
|
||||
18
src/Exception/RateLimitExceededException.php
Normal file
18
src/Exception/RateLimitExceededException.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient\Exception;
|
||||
|
||||
final class RateLimitExceededException extends LicenseException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
?string $errorCode = null,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null,
|
||||
public readonly ?int $retryAfter = null,
|
||||
) {
|
||||
parent::__construct($message, $errorCode, $code, $previous);
|
||||
}
|
||||
}
|
||||
190
src/LicenseClient.php
Normal file
190
src/LicenseClient.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient;
|
||||
|
||||
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
||||
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
||||
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class LicenseClient implements LicenseClientInterface
|
||||
{
|
||||
private const API_PATH = '/wp-json/wc-licensed-product/v1';
|
||||
private const CACHE_TTL = 300; // 5 minutes
|
||||
|
||||
private readonly LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly string $baseUrl,
|
||||
?LoggerInterface $logger = null,
|
||||
private readonly ?CacheItemPoolInterface $cache = null,
|
||||
private readonly int $cacheTtl = self::CACHE_TTL,
|
||||
) {
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
}
|
||||
|
||||
public function validate(string $licenseKey, string $domain): LicenseInfo
|
||||
{
|
||||
$cacheKey = $this->buildCacheKey('validate', $licenseKey, $domain);
|
||||
|
||||
if ($this->cache !== null) {
|
||||
$item = $this->cache->getItem($cacheKey);
|
||||
if ($item->isHit()) {
|
||||
$this->logger->debug('License validation cache hit', [
|
||||
'domain' => $domain,
|
||||
]);
|
||||
return $item->get();
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info('Validating license', [
|
||||
'domain' => $domain,
|
||||
]);
|
||||
|
||||
$response = $this->request('validate', [
|
||||
'license_key' => $licenseKey,
|
||||
'domain' => $domain,
|
||||
]);
|
||||
|
||||
$result = LicenseInfo::fromArray($response['license']);
|
||||
|
||||
if ($this->cache !== null) {
|
||||
$item = $this->cache->getItem($cacheKey);
|
||||
$item->set($result);
|
||||
$item->expiresAfter($this->cacheTtl);
|
||||
$this->cache->save($item);
|
||||
}
|
||||
|
||||
$this->logger->info('License validated successfully', [
|
||||
'domain' => $domain,
|
||||
'product_id' => $result->productId,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function status(string $licenseKey): LicenseStatus
|
||||
{
|
||||
$cacheKey = $this->buildCacheKey('status', $licenseKey);
|
||||
|
||||
if ($this->cache !== null) {
|
||||
$item = $this->cache->getItem($cacheKey);
|
||||
if ($item->isHit()) {
|
||||
$this->logger->debug('License status cache hit');
|
||||
return $item->get();
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info('Checking license status');
|
||||
|
||||
$response = $this->request('status', [
|
||||
'license_key' => $licenseKey,
|
||||
]);
|
||||
|
||||
$result = LicenseStatus::fromArray($response);
|
||||
|
||||
if ($this->cache !== null) {
|
||||
$item = $this->cache->getItem($cacheKey);
|
||||
$item->set($result);
|
||||
$item->expiresAfter($this->cacheTtl);
|
||||
$this->cache->save($item);
|
||||
}
|
||||
|
||||
$this->logger->info('License status retrieved', [
|
||||
'status' => $result->status->value,
|
||||
'valid' => $result->valid,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function activate(string $licenseKey, string $domain): ActivationResult
|
||||
{
|
||||
$this->logger->info('Activating license', [
|
||||
'domain' => $domain,
|
||||
]);
|
||||
|
||||
$response = $this->request('activate', [
|
||||
'license_key' => $licenseKey,
|
||||
'domain' => $domain,
|
||||
]);
|
||||
|
||||
$result = ActivationResult::fromArray($response);
|
||||
|
||||
// Invalidate related cache entries after activation
|
||||
if ($this->cache !== null) {
|
||||
$this->cache->deleteItem($this->buildCacheKey('validate', $licenseKey, $domain));
|
||||
$this->cache->deleteItem($this->buildCacheKey('status', $licenseKey));
|
||||
}
|
||||
|
||||
$this->logger->info('License activation completed', [
|
||||
'domain' => $domain,
|
||||
'success' => $result->success,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LicenseException
|
||||
*/
|
||||
private function request(string $endpoint, array $payload): array
|
||||
{
|
||||
$url = rtrim($this->baseUrl, '/') . self::API_PATH . '/' . $endpoint;
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request('POST', $url, [
|
||||
'json' => $payload,
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$data = $response->toArray(false);
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
$this->logger->warning('License API error response', [
|
||||
'endpoint' => $endpoint,
|
||||
'status_code' => $statusCode,
|
||||
'error' => $data['error'] ?? 'unknown',
|
||||
]);
|
||||
|
||||
throw LicenseException::fromApiResponse($data, $statusCode);
|
||||
}
|
||||
|
||||
return $data;
|
||||
} catch (LicenseException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('License API request failed', [
|
||||
'endpoint' => $endpoint,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw new LicenseException(
|
||||
'Failed to communicate with license server: ' . $e->getMessage(),
|
||||
null,
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildCacheKey(string $operation, string $licenseKey, ?string $domain = null): string
|
||||
{
|
||||
$key = 'wc_license_' . $operation . '_' . hash('sha256', $licenseKey);
|
||||
if ($domain !== null) {
|
||||
$key .= '_' . hash('sha256', $domain);
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
34
src/LicenseClientInterface.php
Normal file
34
src/LicenseClientInterface.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WcLicensedProductClient;
|
||||
|
||||
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
||||
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
||||
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
||||
|
||||
interface LicenseClientInterface
|
||||
{
|
||||
/**
|
||||
* Validate a license key for a specific domain.
|
||||
*
|
||||
* @throws LicenseException When validation fails
|
||||
*/
|
||||
public function validate(string $licenseKey, string $domain): LicenseInfo;
|
||||
|
||||
/**
|
||||
* Get detailed status information for a license key.
|
||||
*
|
||||
* @throws LicenseException When status check fails
|
||||
*/
|
||||
public function status(string $licenseKey): LicenseStatus;
|
||||
|
||||
/**
|
||||
* Activate a license on a specific domain.
|
||||
*
|
||||
* @throws LicenseException When activation fails
|
||||
*/
|
||||
public function activate(string $licenseKey, string $domain): ActivationResult;
|
||||
}
|
||||
Reference in New Issue
Block a user