# WPForms Product API Client

A PHP HTTP client library for interacting with the Product API. This package provides a flexible, middleware-based HTTP client and SDK for some Product API services.

## Features

- **HTTP Client**: Fluent API for making HTTP requests to the Product API
- **Middleware Support**: Extensible middleware system for request/response handling
- **Authentication**: Flexible authentication strategy system
- **Immutable Design**: Client instances are immutable, preventing side effects
- **Product Events SDK**: Built-in SDK for tracking product events
- **Facade Pattern**: Static access to services after configuration

## Installation

Install via Composer:

```bash
composer require awesomemotive/wpforms-product-api-client
```

### Prefixing

**Important:** This package should be prefixed to avoid namespace conflicts with other plugins that may use the same package. We typically use [php-scoper](https://github.com/humbug/php-scoper) for this purpose.

## Initialization

Configure the ProductApi once during plugin load (e.g., on `plugins_loaded` hook):

```php
use ProductApi\ProductApi;

ProductApi::configure( [
    'api_url'        => defined( 'WPMS_PRODUCT_API_BASE_URL' ) ? WPMS_PRODUCT_API_BASE_URL : 'https://wpmailsmtpapi.com',
    'site_url'       => get_site_url(),
    'license_key'    => wp_mail_smtp()->get_license_key(),
    'license_valid'  => wp_mail_smtp()->get_pro()->get_license()->is_valid(),
    'is_pro'         => wp_mail_smtp()->is_pro(),
    'user_agent'     => Helpers::get_default_user_agent(),
    'environment'    => wp_get_environment_type(),
    'plugin_slug'    => 'wp-mail-smtp',
    'plugin_version' => WPMS_PLUGIN_VER,
] )
->with_events( [
    'log_events_cap' => 'manage_options',
] )
->boot();
```

**Important:** 
- Call `configure()` only once during plugin initialization
- Call `boot()` after all configuration (including `with_events()`) is complete
- Calling `configure()` more than once will throw an exception

### Initialization Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `api_url` | string | Yes | Base URL of the Product API |
| `site_url` | string | Yes | Current site URL |
| `license_key` | string | No | License key (Pro only) |
| `license_valid` | bool | No | Whether the license is valid (Pro only) |
| `is_pro` | bool | No | Whether the Pro version is installed |
| `user_agent` | string | No | User agent string for requests |
| `environment` | string | No | Environment type |
| `plugin_slug` | string | Yes | Plugin slug identifier |
| `plugin_version` | string | Yes | Plugin version |

### Enable Product Events

To enable the Product Events SDK, chain the `with_events()` method before `boot()`:

```php
ProductApi::configure( [...] )
    ->with_events( [
        'log_events_cap' => 'manage_options', // Capability required to log events
    ] )
    ->boot();
```

## Using the Facade

After configuration, you can access ProductApi services statically from anywhere in your plugin:

```php
use ProductApi\ProductApi;

// Get the HTTP client
$client = ProductApi::client();

// Get any registered service from the container
$authManager = ProductApi::get( AuthManager::class );
$options     = ProductApi::get( Options::class );
$context     = ProductApi::get( Context::class );

// Get the configured instance
$api = ProductApi::instance();
```

### Available Facade Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `ProductApi::configure( $args )` | `self` | Configure the API (call once, returns instance for chaining) |
| `ProductApi::instance()` | `self` | Get the configured singleton instance |
| `ProductApi::get( $class )` | `mixed` | Resolve a class from the container |
| `ProductApi::client()` | `Client` | Get the HTTP client |

### Instance Methods (for chaining)

| Method | Return Type | Description |
|--------|-------------|-------------|
| `->with_events( $args )` | `self` | Enable events tracking |
| `->boot()` | `self` | Register WordPress hooks |
| `->resolve( $class )` | `mixed` | Resolve a class from the container |

## Using the HTTP Client

Get the client using the static facade:

```php
$client = ProductApi::client();
```

The client is pre-configured with your package settings (API URL, site URL, license key, user agent, etc.).

### Making Requests

The client supports fluent request building:

```php
// GET request
$response = ProductApi::client()->get('/api/v1/endpoint')
    ->query(['param' => 'value'])
    ->send();

```

### Request Methods

#### GET Request

```php
$response = ProductApi::client()->get('/api/v1/users')
    ->query([
        'page' => 1,
        'per_page' => 10,
    ])
    ->send();
```

#### POST Request

```php
$response = ProductApi::client()->post('/api/v1/users')
    ->json([
        'name' => 'John Doe',
        'email' => 'john@example.com',
    ])
    ->send();
```

#### PUT Request

```php
$response = ProductApi::client()->put('/api/v1/users/123')
    ->json([
        'name' => 'Jane Doe',
    ])
    ->send();
```

#### DELETE Request

```php
$response = ProductApi::client()->delete('/api/v1/users/123')
    ->send();
```

### Request Options

#### Set Headers

```php
$response = ProductApi::client()->get('/api/v1/endpoint')
    ->header('X-Custom-Header', 'value')
    ->send();

// Or set multiple headers at once (merges with existing headers)
$response = ProductApi::client()->get('/api/v1/endpoint')
    ->headers([
        'X-Custom-Header-1' => 'value1',
        'X-Custom-Header-2' => 'value2',
    ])
    ->send();
```

#### Set Timeout

```php
$response = ProductApi::client()->get('/api/v1/endpoint')
    ->timeout(30) // 30 seconds
    ->send();
```

#### Non-blocking Requests

```php
$response = ProductApi::client()->post('/api/v1/endpoint')
    ->json(['data' => 'value'])
    ->blocking(false) // Fire and forget
    ->send();
```

#### Custom Base URL

```php
$request = ProductApi::client()->get('/api/v1/endpoint')
    ->base_url('https://custom-api.example.com');
```

### Handling Responses

The `send()` method can return either a `Response` object or a `WP_Error` object. Always check for errors first:

```php
$response = ProductApi::client()->get('/api/v1/endpoint')->send();

// Always check for WP_Error first (rate limits, auth failures, network errors, etc.)
if (is_wp_error($response)) {
    // Handle error
	return;
}

// Check if request was successful
if (!$response->is_successful()) {
	// HTTP error (4xx, 5xx, etc.)
    $errors = $response->get_errors(); // Returns WP_Error
    // Handle error
	return;
}

$data = $response->get_body(); // Automatically decoded JSON
$status_code = $response->get_status_code();
$header = $response->get_header('X-Custom-Header');
```

#### Response Methods

- `is_successful()`: Returns `true` if status code is 200-299
- `get_status_code()`: Returns the HTTP status code
- `get_body()`: Returns the response body (automatically decodes JSON)
- `get_header($name)`: Returns a specific header value
- `get_errors()`: Returns `WP_Error` object if request failed, `false` otherwise

**Important:** The `send()` method returns `Response|WP_Error`. Always check `is_wp_error()` first before using Response methods. It's because the error could be returned form the middleware.

## Middleware

The client supports a middleware system that allows you to intercept and modify requests and responses. Middleware executes in priority order (lower numbers execute first).

### Built-in Middleware

#### Rate Limit Middleware

Limits the number of requests per time period:

```php
use ProductApi\ProductApi;
use ProductApi\Http\Middleware\RateLimitMiddleware;
use ProductApi\Options;

$options = ProductApi::get( Options::class );

$rate_limit = new RateLimitMiddleware(
    'api_requests',           // Action name
    $options,                 // Options instance
    10,                       // Max requests
    60,                       // Time period in seconds
    'Too many requests!'      // Error message (optional)
);

$client = ProductApi::client()->middleware($rate_limit, 10);
```

#### Lock Middleware

Prevents concurrent execution of requests:

```php
use ProductApi\ProductApi;
use ProductApi\Http\Middleware\LockMiddleware;
use ProductApi\Options;

$options = ProductApi::get( Options::class );

$lock = new LockMiddleware(
    'register_site',         // Action name
    $options,                 // Options instance
    MINUTE_IN_SECONDS,        // Lock expiration (default: 60 seconds)
    'Operation in progress!' // Error message (optional)
);

$client = ProductApi::client()->middleware($lock, 5);
```

### Creating Custom Middleware

Implement the `MiddlewareInterface`:

```php
use ProductApi\Http\Middleware\MiddlewareInterface;
use ProductApi\Http\Request;
use ProductApi\Http\Response;
use WP_Error;

class CustomMiddleware implements MiddlewareInterface {
    
    public function handle(Request $request, callable $next) {
        // Modify request before it's sent
        $request->header('X-Custom-Header', 'value');
        
        // Execute the next middleware or the actual request
        $response = $next($request);
        
        // Modify response if needed
        if ($response instanceof Response) {
            // Process response
        }
        
        return $response;
    }
}
```

### Using Middleware

#### Client-level Middleware

Apply middleware to all requests made with a client:

```php
$client = ProductApi::client()->middleware($middleware, $priority);
```

#### Request-level Middleware

Apply middleware to a specific request:

```php
$response = ProductApi::client()->get('/api/v1/endpoint')
    ->middleware($middleware, $priority)
    ->send();
```

### Middleware Priority

Middleware executes in priority order (lower numbers first). The default priority is `10`.

```php
$client = ProductApi::client();

// This middleware executes first (priority 5)
$client = $client->middleware($middleware1, 5);

// This middleware executes second (priority 10)
$client = $client->middleware($middleware2, 10);

// This middleware executes last (priority 20)
$client = $client->middleware($middleware3, 20);
```

## Authentication

The client supports authentication strategies. Authentication middleware is automatically applied when an auth strategy is set.

### Setting Authentication Strategy

```php
use ProductApi\Auth\AbstractAuthStrategy;

class CustomAuthStrategy extends AbstractAuthStrategy {
    public function authenticate_request(Request $request) {
        $request->header('Authorization', 'Bearer token');
        return true;
    }
}

$client = ProductApi::client()->auth_strategy(new CustomAuthStrategy());
```

### Request-specific Authentication

```php
$response = ProductApi::client()->get('/api/v1/endpoint')
    ->auth_strategy($custom_auth_strategy)
    ->send();
```

### Using HMAC Authentication

The package includes a built-in HMAC authentication strategy for signing requests:

```php
use ProductApi\ProductApi;
use ProductApi\Auth\HMACAuthStrategy;

// Get HMAC auth strategy from the container
$hmac_auth = ProductApi::get( HMACAuthStrategy::class );

// Use at client level (applies to all requests)
$client = ProductApi::client()->auth_strategy($hmac_auth);

// Or use at request level (applies to specific request)
$response = ProductApi::client()->get('/api/v1/endpoint')
    ->auth_strategy($hmac_auth)
    ->send();
```

The HMAC authentication strategy:
- Generates HMAC signatures for each request
- Adds required authentication headers (`X-Site-Id`, `X-Installation-Id`, `X-Verification-Key`, `X-Timestamp`, `X-Signature`)
- Returns `WP_Error` with code `site_not_registered` if the site hasn't been registered

### Site Registration

Before using HMAC authentication, the site must be registered explicitly using the `SiteRegistration` class:

```php
use ProductApi\ProductApi;
use ProductApi\Auth\SiteRegistration;

$registration = ProductApi::get( SiteRegistration::class );

// Check if the site is already registered
if ( ! $registration->is_registered() ) {
    // Register with contact info from subscription form
    $result = $registration->register( [
        'contact' => [
            'email'      => 'user@example.com',
            'first_name' => 'John',
            'last_name'  => 'Doe',
        ],
    ] );

    // Or register without contact info (falls back to current user data)
    $result = $registration->register();

    if ( is_wp_error( $result ) ) {
        // Handle registration error
        $error_message = $result->get_error_message();
    }
}
```

The `SiteRegistration` class:
- `is_registered()` - Returns `true` if the site has valid credentials
- `register( array $args = [] )` - Registers the site with the API. Accepts optional `contact` array with `email`, `first_name`, `last_name`. Falls back to current user data if not provided.

## Product Events SDK

The package includes an SDK for tracking product events, primarily designed for client-side (JavaScript) event tracking.

### Enabling Events

```php
ProductApi::configure( [...] )
    ->with_events( [
        'log_events_cap' => 'manage_options', // Capability required to log events
    ] )
    ->boot();
```

When events are enabled, the package automatically:
- Registers an AJAX endpoint for JavaScript event tracking
- Registers a JavaScript function for client-side event logging (you need to enqueue it manually)

### Client-Side Event Tracking (JavaScript)

After enabling events, you need to enqueue the script on the pages where you want to track events:

```php
// Enqueue the script (typically in your admin_enqueue_scripts or wp_enqueue_scripts hook)
// The handle format is: {plugin_slug}-product-events
// Example: 'wp-mail-smtp-product-events' for plugin slug 'wp-mail-smtp'
wp_enqueue_script('your-plugin-slug-product-events');
```

After enqueuing the script, a global JavaScript function is automatically created for tracking events from the browser:

```javascript
// The function name is based on your plugin identifier
// Format: {plugin_identifier}_log
// Example: wp_mail_smtp_log, wpforms_log, etc.

// Track an event
wp_mail_smtp_log('button_clicked', {
    button_id: 'save-settings',
    page: 'settings'
});

// Track another event
wp_mail_smtp_log('form_submitted', {
    form_id: 123,
    fields_count: 5
});
```

The function name is dynamically generated based on your `plugin_slug` (with hyphens replaced by underscores). For example:
- Plugin slug: `wp-mail-smtp` → Function: `wp_mail_smtp_log`
- Plugin slug: `wpforms` → Function: `wpforms_log`

### Server-Side Event Tracking (PHP)

For server-side event tracking, get the event tracker from the container:

```php
use ProductApi\ProductApi;
use ProductApi\Events\Event;
use ProductApi\Events\EventsManager;

// Get the events manager and tracker
$events_manager = ProductApi::get( EventsManager::class );
$tracker = $events_manager->get_tracker();

// Track an event
$event = new Event('plugin_activated', [
    'plugin_name' => 'my-plugin',
    'version' => '1.0.0',
]);

$tracker->track($event);
```

**Note:** The `EventsManager` is only available if events have been enabled via `with_events()`. Make sure to call `with_events()` before accessing the tracker.

### Event Context

Events can have different contexts depending on who or what triggered them:

**User Context** (default): Events triggered on behalf of a user, typically from client-side interactions. These events include user information (ID, email, name, locale) in the payload.

```php
// User clicked a button (client-side event)
$event = new Event('button_clicked', [
    'button_id' => 'save-settings',
    'page' => 'settings'
])
    ->context('user');

// User submitted a form
$event = new Event('form_submitted', [
    'form_id' => 123
])
    ->context('user');
```

**System Context**: Events triggered by the system automatically, usually from server-side processes. These events do not include user information.

```php
// System event: milestone reached (e.g., 1000 emails sent)
$event = new Event('milestone_reached', [
    'milestone' => 'emails_sent',
    'count' => 1000
])
    ->context('system');

// System event: scheduled task completed
$event = new Event('scheduled_task_completed', [
    'task_name' => 'daily_report',
    'duration' => 45
])
    ->context('system');
```

### Event Properties

```php
$event = new Event('form_submitted', [
    'form_id' => 123,
    'fields_count' => 5,
    'submission_time' => time(),
]);
```

**Note:** Events tracked via `EventTracker::track()` are sent asynchronously (non-blocking) with a 1-second timeout to avoid impacting page performance.


## Architecture

### Facade Pattern

The ProductApi uses a facade pattern that stores a singleton instance after configuration. This allows:

1. **Fluent configuration** during initialization:
   ```php
   ProductApi::configure([...])->with_events([...])->boot();
   ```

2. **Static access** from anywhere after configuration:
   ```php
   $client = ProductApi::client();
   $service = ProductApi::get(SomeClass::class);
   ```

### Immutable Client Pattern

The client uses an immutable pattern. When you modify a client (e.g., add middleware or set auth strategy), a new client instance is returned. This prevents side effects and allows safe reuse of the base client.

```php
$base_client = ProductApi::client();

// This returns a NEW client instance
$client_with_middleware = $base_client->middleware($middleware);

// $base_client remains unchanged
```

## Requirements

- PHP >= 7.2
