Cookbook
Service Options Resolver
Validate and normalize per-service runtime options with Glueful’s internal SimpleOptionsResolver.
Overview
Glueful provides an internal options resolver (SimpleOptionsResolver) for robust per-service option validation — type-safe defaults, validation, and normalization without requiring Symfony's OptionsResolver.
Quick Start
Basic Service Configuration
use Glueful\Support\Options\ConfigurableService;
use Glueful\Support\Options\SimpleOptionsResolver as OptionsResolver;
class MyService extends ConfigurableService
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'timeout' => 30,
'retries' => 3,
'debug' => false,
]);
$resolver->setRequired(['api_key']);
$resolver->setAllowedTypes('timeout', 'int');
$resolver->setAllowedTypes('retries', 'int');
$resolver->setAllowedTypes('debug', 'bool');
$resolver->setAllowedTypes('api_key', 'string');
}
}
// Usage
$service = new MyService([
'api_key' => 'secret-key',
'timeout' => 60,
'debug' => true,
]);
Using ConfigurableTrait
use Glueful\Support\Options\ConfigurableInterface;
use Glueful\Support\Options\ConfigurableTrait;
use Glueful\Support\Options\SimpleOptionsResolver as OptionsResolver;
class DatabaseService implements ConfigurableInterface
{
use ConfigurableTrait;
public function __construct(array $config = [])
{
$this->resolveOptions($config);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'host' => 'localhost',
'port' => 3306,
'charset' => 'utf8mb4',
'timeout' => 30,
]);
$resolver->setRequired(['database', 'username']);
$resolver->setAllowedTypes('port', 'int');
$resolver->setAllowedValues('port', function($value) {
return $value >= 1 && $value <= 65535;
});
}
public function connect(): void
{
$host = $this->getOption('host');
$port = $this->getOption('port');
// ... connection logic
}
}
Configuration Validation Features
1. Type Validation
$resolver->setAllowedTypes('port', 'int');
$resolver->setAllowedTypes('host', 'string');
$resolver->setAllowedTypes('options', 'array');
$resolver->setAllowedTypes('callback', 'callable');
$resolver->setAllowedTypes('enabled', 'bool');
// Multiple types allowed
$resolver->setAllowedTypes('timeout', ['int', 'float']);
$resolver->setAllowedTypes('password', ['string', 'null']);
2. Value Constraints
// Enum-style validation
$resolver->setAllowedValues('env', ['development', 'staging', 'production']);
// Custom validation functions
$resolver->setAllowedValues('port', function($value) {
return $value >= 1024 && $value <= 65535;
});
$resolver->setAllowedValues('memory_limit', function($value) {
return is_string($value) && preg_match('/^\d+[KMG]?$/', $value);
});
3. Required vs Optional
// Required options (will throw exception if missing)
$resolver->setRequired(['database', 'username', 'password']);
// Optional with defaults
$resolver->setDefaults([
'host' => 'localhost',
'port' => 3306,
'timeout' => 30,
]);
4. Value Normalization
// Clean and normalize values
$resolver->setNormalizer('host', function($options, $value) {
return trim(strtolower($value));
});
$resolver->setNormalizer('tags', function($options, $value) {
if (is_string($value)) {
$value = explode(',', $value);
}
return array_map('trim', $value);
});
// Cross-option validation
$resolver->setNormalizer('max_connections', function($options, $value) {
if ($value < $options['min_connections']) {
throw new \InvalidArgumentException(
'max_connections must be >= min_connections'
);
}
return $value;
});
Real-World Examples
1. Queue Configuration (Example)
class QueueConfigurable extends ConfigurableService
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'default' => 'database',
'connections' => [],
'failed' => [
'driver' => 'database',
'table' => 'queue_failed_jobs',
],
'workers' => [
'auto_scale' => false,
'min_workers' => 1,
'max_workers' => 10,
],
]);
$resolver->setRequired(['default', 'connections']);
// Validate that default connection exists
$resolver->setNormalizer('default', function($options, $value) {
if (!isset($options['connections'][$value])) {
throw new \InvalidArgumentException(
"Default connection '{$value}' not found"
);
}
return $value;
});
}
}
2. Notification Service (Used in framework)
class NotificationService implements ConfigurableInterface
{
use ConfigurableTrait;
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'default_channels' => ['database'],
'max_retry_attempts' => 3,
'retry_delay_seconds' => 60,
'rate_limit_per_minute' => 1000,
'id_generator' => function() {
return Utils::generateNanoID();
},
]);
// Validate channels
$resolver->setNormalizer('default_channels', function($options, $value) {
$validChannels = ['email', 'sms', 'database', 'slack', 'webhook'];
foreach ($value as $channel) {
if (!in_array($channel, $validChannels)) {
throw new \InvalidArgumentException(
"Invalid channel '{$channel}'"
);
}
}
return array_unique($value);
});
// Test ID generator
$resolver->setNormalizer('id_generator', function($options, $value) {
$testId = $value();
if (!is_string($testId) || empty($testId)) {
throw new \InvalidArgumentException(
'id_generator must return non-empty string'
);
}
return $value;
});
}
}
3. Connection Pool Configuration (Used in framework)
class ConfigurableConnectionPool extends ConnectionPool
{
use ConfigurableTrait;
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'min_connections' => 2,
'max_connections' => 10,
'idle_timeout' => 300,
'acquisition_timeout' => 30,
]);
// Range validation
$resolver->setAllowedValues('min_connections', function($value) {
return $value >= 1 && $value <= 100;
});
// Cross-validation
$resolver->setNormalizer('max_connections', function($options, $value) {
if ($value < $options['min_connections']) {
throw new \InvalidArgumentException(
'max_connections must be >= min_connections'
);
}
return $value;
});
}
}
Creating Configurable Services
Option 1: Extend ConfigurableService
use Glueful\Support\Options\ConfigurableService;
class EmailService extends ConfigurableService
{
private $mailer;
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'host' => 'localhost',
'port' => 587,
'encryption' => 'tls',
'timeout' => 60,
]);
$resolver->setRequired(['username', 'password']);
$resolver->setAllowedValues('encryption', ['tls', 'ssl', null]);
$resolver->setAllowedValues('port', function($value) {
return in_array($value, [25, 465, 587, 2525]);
});
}
public function send(string $to, string $subject, string $body): bool
{
$host = $this->getOption('host');
$port = $this->getOption('port');
// ... email sending logic
}
}
// Usage
$emailService = new EmailService([
'host' => 'smtp.example.com',
'username' => 'user@example.com',
'password' => 'secret',
'port' => 587,
]);
Option 2: Use ConfigurableTrait
use Glueful\Support\Options\ConfigurableInterface;
use Glueful\Support\Options\ConfigurableTrait;
use Glueful\Support\Options\SimpleOptionsResolver as OptionsResolver;
class CacheService implements ConfigurableInterface
{
use ConfigurableTrait;
public function __construct(array $config = [])
{
$this->resolveOptions($config);
$this->initializeCache();
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'driver' => 'redis',
'prefix' => 'cache_',
'ttl' => 3600,
'serializer' => 'json',
]);
$resolver->setAllowedValues('driver', ['redis', 'file', 'memory']);
$resolver->setAllowedValues('serializer', ['json', 'serialize', 'none']);
$resolver->setAllowedTypes('ttl', 'int');
$resolver->setAllowedValues('ttl', function($value) {
return $value > 0 && $value <= 86400; // 1 second to 24 hours
});
}
private function initializeCache(): void
{
$driver = $this->getOption('driver');
$prefix = $this->getOption('prefix');
// ... cache initialization
}
}
Advanced Configuration Patterns
1. Nested Configuration
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'database' => [
'host' => 'localhost',
'port' => 3306,
],
'cache' => [
'driver' => 'redis',
'ttl' => 3600,
],
]);
// Validate nested arrays
$resolver->setNormalizer('database', function($options, $value) {
$dbResolver = new OptionsResolver();
$dbResolver->setDefaults(['host' => 'localhost', 'port' => 3306]);
$dbResolver->setRequired(['username', 'password']);
$dbResolver->setAllowedTypes('port', 'int');
return $dbResolver->resolve($value);
});
}
2. Environment-Based Defaults
public function configureOptions(OptionsResolver $resolver): void
{
$isDev = ($_ENV['APP_ENV'] ?? 'production') === 'development';
$resolver->setDefaults([
'debug' => $isDev,
'timeout' => $isDev ? 0 : 30, // No timeout in dev
'cache_enabled' => !$isDev,
'log_level' => $isDev ? 'debug' : 'error',
]);
}
3. Conditional Validation
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'ssl_enabled' => false,
'ssl_cert' => null,
'ssl_key' => null,
]);
// Require SSL files when SSL is enabled
$resolver->setNormalizer('ssl_cert', function($options, $value) {
if ($options['ssl_enabled'] && empty($value)) {
throw new \InvalidArgumentException(
'ssl_cert is required when ssl_enabled is true'
);
}
return $value;
});
}
4. Performance Scoring
public function configureOptions(OptionsResolver $resolver): void
{
// ... other configuration
$resolver->setNormalizer('max_connections', function($options, $value) {
if ($value > 50) {
trigger_error(
'High connection count may impact performance',
E_USER_NOTICE
);
}
return $value;
});
}
public function getPerformanceScore(): int
{
$config = $this->getOptions();
$score = 100;
if ($config['max_connections'] > 50) $score -= 20;
if ($config['timeout'] > 60) $score -= 10;
if (!$config['cache_enabled']) $score -= 15;
return max(0, $score);
}
Validation Examples
1. URL Validation
$resolver->setNormalizer('api_url', function($options, $value) {
if (!filter_var($value, FILTER_VALIDATE_URL)) {
throw new \InvalidArgumentException('Invalid URL format');
}
return rtrim($value, '/'); // Remove trailing slash
});
2. File Path Validation
$resolver->setNormalizer('log_path', function($options, $value) {
$dir = dirname($value);
if (!is_dir($dir)) {
throw new \InvalidArgumentException("Directory does not exist: {$dir}");
}
if (!is_writable($dir)) {
throw new \InvalidArgumentException("Directory not writable: {$dir}");
}
return $value;
});
3. Memory Limit Validation
$resolver->setNormalizer('memory_limit', function($options, $value) {
if (!preg_match('/^(\d+)([KMG]?)$/i', $value, $matches)) {
throw new \InvalidArgumentException('Invalid memory limit format');
}
$size = (int)$matches[1];
$unit = strtoupper($matches[2] ?? '');
$bytes = $size * match($unit) {
'K' => 1024,
'M' => 1024 * 1024,
'G' => 1024 * 1024 * 1024,
default => 1
};
if ($bytes > 2 * 1024 * 1024 * 1024) { // 2GB
throw new \InvalidArgumentException('Memory limit too high');
}
return $value;
});
Migrating Existing Services
Before (Manual Validation)
class OldService
{
private array $config;
public function __construct(array $config = [])
{
// Manual validation
$this->config = array_merge([
'timeout' => 30,
'retries' => 3,
], $config);
if (!isset($config['api_key'])) {
throw new \InvalidArgumentException('api_key is required');
}
if (!is_int($this->config['timeout']) || $this->config['timeout'] <= 0) {
throw new \InvalidArgumentException('timeout must be positive integer');
}
// ... more validation
}
}
After (Glueful OptionsResolver)
class NewService extends ConfigurableService
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'timeout' => 30,
'retries' => 3,
]);
$resolver->setRequired(['api_key']);
$resolver->setAllowedTypes('timeout', 'int');
$resolver->setAllowedValues('timeout', function($value) {
return $value > 0;
});
$resolver->setAllowedTypes('retries', 'int');
$resolver->setAllowedValues('retries', function($value) {
return $value >= 0 && $value <= 10;
});
}
}
Error Handling
Common Validation Errors
try {
$service = new MyService($config);
} catch (\\InvalidArgumentException $e) {
// Configuration error (invalid type/value, missing/unknown option)
echo "Configuration error: " . $e->getMessage();
}
echo "Unknown configuration option: " . $e->getMessage();
}
Custom Error Messages
$resolver->setNormalizer('port', function($options, $value) {
if (!is_int($value) || $value < 1 || $value > 65535) {
throw new \InvalidArgumentException(
"Port must be an integer between 1 and 65535, got: " .
(is_scalar($value) ? $value : gettype($value))
);
}
return $value;
});
Best Practices
- Always set sensible defaults
- Use type validation for all options
- Validate ranges and constraints
- Provide clear error messages
- Test your configuration validation
- Document available options
- Use cross-validation for related options
- Consider environment-specific defaults
Benefits
- ✅ Type Safety: Automatic type checking
- ✅ Validation: Custom constraint validation
- ✅ Defaults: Sensible fallback values
- ✅ Documentation: Self-documenting configuration
- ✅ Error Messages: Clear validation errors
- ✅ Normalization: Automatic value cleanup
- ✅ IDE Support: Better autocomplete and hints
- ✅ Consistency: Standardized configuration across services
This integration makes Glueful services more robust, user-friendly, and maintainable!