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]
|
## [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
|
## [0.0.1] - 2026-01-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -29,9 +29,16 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
No known bugs at the moment
|
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
|
## Technical Stack
|
||||||
|
|
||||||
- **Language:** PHP 8.3.x
|
- **Language:** PHP 8.3.x
|
||||||
|
- **PHP-Standards:** PSR-3, PSR-4, PSR-6, PSR-18
|
||||||
- **Coding-Style:** Symfony
|
- **Coding-Style:** Symfony
|
||||||
- **HTTP-Client-Library:** symfony/http-client
|
- **HTTP-Client-Library:** symfony/http-client
|
||||||
- **Dependency Management:** Composer
|
- **Dependency Management:** Composer
|
||||||
|
|||||||
90
README.md
90
README.md
@@ -15,12 +15,96 @@ composer require magdev/wc-licensed-product-client
|
|||||||
|
|
||||||
## Features
|
## 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 validation against domains
|
||||||
- License activation on domains
|
- License activation on domains
|
||||||
- License status checking
|
- License status checking
|
||||||
|
- Comprehensive exception handling
|
||||||
- Built on Symfony HttpClient
|
- 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
|
## API Endpoints
|
||||||
|
|
||||||
This client interacts with the following WooCommerce Licensed Product 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 /status** - Get detailed license status information
|
||||||
- **POST /activate** - Activate a license on a domain
|
- **POST /activate** - Activate a license on a domain
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Coming soon in future versions.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-2.0-or-later
|
GPL-2.0-or-later
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
|
"psr/cache": "^3.0",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"psr/log": "^3.0",
|
||||||
"symfony/http-client": "^7.0"
|
"symfony/http-client": "^7.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "dabb6ce648c8b638dc4b20b8999dea1d",
|
"content-hash": "e412380889a4c25cef1aa8d453216223",
|
||||||
"packages": [
|
"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",
|
"name": "psr/container",
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
@@ -59,6 +108,111 @@
|
|||||||
},
|
},
|
||||||
"time": "2021-11-05T16:47:00+00:00"
|
"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",
|
"name": "psr/log",
|
||||||
"version": "3.0.2",
|
"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