moonspot/phlag-client

Client library for reading feature flags from the Phlag application

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/moonspot/phlag-client

0.1.0 2025-11-23 00:00 UTC

README

PHP client library for the Phlag feature flag management system

This library provides a simple, type-safe interface for querying feature flags from a Phlag server. It handles authentication, environment management, and error handling so you can focus on feature rollouts.

Features

  • 🎯 Type-safe flag retrieval - Get boolean, integer, float, or string values
  • 🌐 Environment-aware - Configure once, query a specific environment
  • 🔄 Immutable environment switching - Easy multi-environment queries
  • Simple API - Clean, fluent interface with convenience methods
  • 🛡️ Robust error handling - Specific exceptions for different error conditions
  • Fully tested - Comprehensive test coverage with PHPUnit

Requirements

  • PHP 8.2 or higher
  • Composer
  • A running Phlag server instance

Installation

Install via Composer:

composer require moonspot/phlag-client

Quick Start

use Moonspot\PhlagClient\PhlagClient;

// Create a client for a specific environment
$client = new PhlagClient(
    base_url: 'http://localhost:8000',
    api_key: 'your-64-character-api-key',
    environment: 'production'
);

// Check if a feature is enabled
if ($client->isEnabled('feature_checkout')) {
    // Show the new checkout flow
}

// Get typed configuration values
$max_items = $client->getFlag('max_items'); // returns int or null
$price_multiplier = $client->getFlag('price_multiplier'); // returns float or null
$welcome_message = $client->getFlag('welcome_message'); // returns string or null

// For high-traffic apps, enable caching
$cached_client = new PhlagClient(
    base_url: 'http://localhost:8000',
    api_key: 'your-api-key',
    environment: 'production',
    cache: true  // Fetches all flags once, caches for 5 minutes
);

Usage

Creating a Client

The PhlagClient constructor requires three parameters:

$client = new PhlagClient(
    base_url: 'http://phlag.example.com',    // Your Phlag server URL
    api_key: 'your-api-key-here',            // 64-char API key from Phlag admin
    environment: 'production'                 // Environment to query
);

Subdirectory Installation

If your Phlag server is installed in a subdirectory, include the full path in the base URL:

$client = new PhlagClient(
    base_url: 'https://www.example.com/phlag',  // Note: includes /phlag subdirectory
    api_key: 'your-api-key',
    environment: 'production'
);

// Client will correctly request:
// https://www.example.com/phlag/flag/production/feature_name

The client automatically handles trailing slashes, so both https://www.example.com/phlag and https://www.example.com/phlag/ work correctly.

Checking Feature Flags

The isEnabled() method is perfect for boolean feature toggles:

if ($client->isEnabled('feature_new_dashboard')) {
    // Feature is active
}

Heads-up: isEnabled() returns true only for actual boolean true values. Non-existent flags, inactive flags, and non-boolean values all return false.

Getting Flag Values

Use getFlag() to retrieve any flag type:

// SWITCH flags return bool
$enabled = $client->getFlag('feature_checkout'); // true or false

// INTEGER flags return int or null
$max_items = $client->getFlag('max_items'); // 100 or null

// FLOAT flags return float or null
$multiplier = $client->getFlag('price_multiplier'); // 1.5 or null

// STRING flags return string or null
$message = $client->getFlag('welcome_message'); // "Hello!" or null

When flags return null:

  • The flag doesn't exist
  • The flag isn't configured for the environment
  • The flag is outside its temporal constraints (start/end dates)

Note: SWITCH flags return false when inactive, not null.

Working with Multiple Environments

You can switch environments without creating new client instances:

$prod_client = new PhlagClient(
    base_url: 'http://phlag.example.com',
    api_key: 'your-api-key',
    environment: 'production'
);

// Create a new client for staging (immutable pattern)
$staging_client = $prod_client->withEnvironment('staging');

// Original client unchanged
echo $prod_client->getEnvironment(); // "production"
echo $staging_client->getEnvironment(); // "staging"

// Query both environments
$prod_enabled = $prod_client->isEnabled('feature_beta');
$staging_enabled = $staging_client->isEnabled('feature_beta');

Performance & Caching

For high-traffic applications, enable file-based caching to dramatically reduce API calls:

$client = new PhlagClient(
    base_url: 'http://localhost:8000',
    api_key: 'your-api-key',
    environment: 'production',
    cache: true,           // Enable caching
    cache_file: null,      // Auto-generate filename (optional)
    cache_ttl: 300         // Cache for 5 minutes (optional)
);

// First call fetches all flags from API (1 request)
$enabled = $client->isEnabled('feature_checkout');

// Subsequent calls use cached data (0 requests)
$max = $client->getFlag('max_items');
$price = $client->getFlag('price_multiplier');

How Caching Works

When caching is enabled:

  1. First request: Client fetches ALL flags for the environment via /all-flags endpoint
  2. Cache storage: Flags stored in memory AND persisted to disk
  3. Subsequent requests: Served from in-memory cache (no API calls)
  4. Cache expiration: After TTL expires, next request refreshes from API
  5. Cross-request persistence: Cache file survives between PHP requests

Cache File Location

By default, cache files are stored in the system temp directory with auto-generated names:

// Auto-generated filename format
sys_get_temp_dir() . '/phlag_cache_{hash}.json'

// Hash is MD5 of base_url + environment
// Example: /tmp/phlag_cache_a1b2c3d4e5f6.json

Custom cache file:

$client = new PhlagClient(
    base_url: 'http://localhost:8000',
    api_key: 'your-api-key',
    environment: 'production',
    cache: true,
    cache_file: '/var/cache/app/phlag_prod.json'
);

Cache Management

Warming the cache (preload before first request):

$client->warmCache();  // Immediately fetches and caches all flags

Clearing the cache (force fresh fetch):

$client->clearCache();  // Removes cache file and in-memory data

// Next request will fetch fresh from API
$value = $client->getFlag('feature');

Checking cache status:

if ($client->isCacheEnabled()) {
    echo "Cache file: " . $client->getCacheFile() . "\n";
    echo "TTL: " . $client->getCacheTtl() . " seconds\n";
}

When to Use Caching

✅ Good use cases:

  • High-traffic applications with frequent flag checks
  • Flags that change infrequently (hourly, daily)
  • Reducing API load and network latency
  • Improving response times (sub-millisecond flag checks)

❌ When to avoid caching:

  • You need real-time flag updates (seconds matter)
  • Flags change very frequently
  • Low-traffic applications (caching overhead not worth it)
  • Single flag check per request

Performance Impact

Without caching:

  • API calls: N (one per getFlag() call)
  • Network overhead: ~10-50ms per call
  • Total overhead: N × 10-50ms

With caching:

  • API calls: 1 per TTL period (default 5 minutes)
  • First request: ~10-50ms (fetch all flags)
  • Subsequent requests: <1ms (memory lookup)
  • Cache file I/O: ~1-2ms on first load per PHP request

Example savings (100 flag checks per request, 1000 requests/minute):

  • Without cache: 100,000 API calls/minute
  • With cache (300s TTL): ~20 API calls/minute (99.98% reduction)

Error Handling

The client throws specific exceptions for different error conditions:

use Moonspot\PhlagClient\Exception\AuthenticationException;
use Moonspot\PhlagClient\Exception\InvalidEnvironmentException;
use Moonspot\PhlagClient\Exception\InvalidFlagException;
use Moonspot\PhlagClient\Exception\NetworkException;
use Moonspot\PhlagClient\Exception\PhlagException;

try {
    $value = $client->getFlag('my_flag');
} catch (AuthenticationException $e) {
    // Invalid API key (401)
    error_log('Bad API key: ' . $e->getMessage());
} catch (InvalidFlagException $e) {
    // Flag doesn't exist (404)
    error_log('Flag not found: ' . $e->getMessage());
} catch (InvalidEnvironmentException $e) {
    // Environment doesn't exist (404)
    error_log('Environment not found: ' . $e->getMessage());
} catch (NetworkException $e) {
    // Connection failed, timeout, etc.
    error_log('Network error: ' . $e->getMessage());
} catch (PhlagException $e) {
    // Other errors (500, etc.)
    error_log('Phlag error: ' . $e->getMessage());
}

All exceptions extend PhlagException, so you can catch them all with a single block:

try {
    $value = $client->getFlag('my_flag');
} catch (PhlagException $e) {
    // Handle any Phlag error
    error_log('Error fetching flag: ' . $e->getMessage());
    $value = null; // Use a safe default
}

API Reference

PhlagClient

__construct(string $base_url, string $api_key, string $environment, bool $cache = false, ?string $cache_file = null, int $cache_ttl = 300)

Creates a new client instance.

Parameters:

  • $base_url - Base URL of your Phlag server (e.g., http://localhost:8000)
  • $api_key - 64-character API key from the Phlag admin panel
  • $environment - Environment name (e.g., production, staging, development)
  • $cache - Enable file-based caching (default: false)
  • $cache_file - Custom cache file path (default: auto-generated in system temp directory)
  • $cache_ttl - Cache time-to-live in seconds (default: 300)

getFlag(string $name): mixed

Retrieves a flag value. When caching is enabled, serves from cache after first request.

Parameters:

  • $name - Flag name

Returns: The flag value (bool, int, float, string, or null)

Throws:

  • AuthenticationException - Invalid API key
  • InvalidFlagException - Flag doesn't exist (cache disabled only)
  • InvalidEnvironmentException - Environment doesn't exist
  • NetworkException - Network communication failed
  • PhlagException - Other errors

isEnabled(string $name): bool

Convenience method for checking SWITCH flags.

Parameters:

  • $name - Flag name

Returns: true if the flag value is boolean true, false otherwise

Throws: Same as getFlag()

getEnvironment(): string

Gets the current environment name.

Returns: The environment name

withEnvironment(string $environment): self

Creates a new client for a different environment. Cache settings are preserved but a new cache file is generated.

Parameters:

  • $environment - New environment name

Returns: New PhlagClient instance (immutable pattern)

warmCache(): void

Preloads the flag cache immediately. Useful for warming cache during application startup.

Heads-up: No-op if caching is disabled.

Throws: Same as getFlag() for API errors

clearCache(): void

Clears in-memory and file cache, forcing fresh fetch on next request.

Heads-up: No-op if caching is disabled.

isCacheEnabled(): bool

Checks if caching is enabled.

Returns: true if caching is enabled

getCacheFile(): string

Gets the cache file path (even if file doesn't exist yet).

Returns: Absolute path to the cache file

getCacheTtl(): int

Gets the cache time-to-live in seconds.

Returns: Cache TTL in seconds

Development

Running Tests

# Install dependencies
composer install

# Run all tests
composer test

# Run just unit tests
composer unit

# Run just linting
composer lint

# Fix code style
composer fix

Project Structure

phlag-client/
├── src/
│   ├── Client.php              # HTTP client wrapper
│   ├── PhlagClient.php         # Main public API
│   └── Exception/              # Exception hierarchy
├── tests/
│   ├── ClientTest.php          # Client tests
│   └── PhlagClientTest.php     # PhlagClient tests
└── composer.json

Troubleshooting

404 Errors with Subdirectory Installation

If you're getting 404 errors and your Phlag server is installed in a subdirectory:

Problem: Base URL doesn't include the subdirectory path
Solution: Make sure your base URL includes the full path to Phlag

// Wrong - missing subdirectory
$client = new PhlagClient('https://www.example.com', 'key', 'prod');

// Correct - includes /phlag subdirectory
$client = new PhlagClient('https://www.example.com/phlag', 'key', 'prod');

Stale Cache Data

If you're seeing outdated flag values when caching is enabled:

Problem: Cache hasn't expired yet
Solutions:

  1. Use shorter TTL: new PhlagClient($url, $key, $env, cache: true, cache_ttl: 60)
  2. Manually clear: $client->clearCache()
  3. Disable caching if you need real-time updates

Cache File Permission Errors

If cache files aren't being created:

Problem: No write permission to temp directory or custom cache path
Solution: Ensure PHP has write access to sys_get_temp_dir() or your custom cache directory

// Check permissions
$cache_file = $client->getCacheFile();
$cache_dir = dirname($cache_file);
if (!is_writable($cache_dir)) {
    echo "No write permission to: $cache_dir\n";
}

Heads-up: Cache write failures are logged but don't throw exceptions. The client gracefully degrades to non-cached operation.

Connection Timeouts

The default timeout is 10 seconds. If you're experiencing timeouts with a slow network or heavily loaded server, you may need to adjust Guzzle's timeout configuration by extending the Client class.

Invalid API Key Errors

Make sure you're using the full 64-character API key exactly as shown in the Phlag admin panel. Keys are case-sensitive and must be copied completely.

Contributing

Contributions welcome! This project follows strict coding standards:

  • PSR-1 and PSR-12 compliance
  • 1TBS brace style
  • snake_case for variables/properties
  • camelCase for methods
  • Type declarations on all methods
  • Protected visibility (not private) unless truly encapsulated
  • PHPDoc blocks in conversational style

Run composer fix before committing to ensure code style compliance.

License

BSD 3-Clause License - see LICENSE file for details.

Credits

Built by Brian Moon (brian@moonspot.net)

Dependencies:

Support

For bugs and feature requests, please use the GitHub issue tracker.

For questions, contact brian@moonspot.net.