diff --git a/public/docs/packages/config/exceptions/config-exception.md b/public/docs/packages/config/exceptions/config-exception.md new file mode 100644 index 0000000..3482921 --- /dev/null +++ b/public/docs/packages/config/exceptions/config-exception.md @@ -0,0 +1,341 @@ +--- +id: config-exception-config-exception +slug: docs/packages/config/exceptions/config-exception +title: ConfigException Class +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The ConfigException class is thrown when configuration operations fail. +llm_summary: > + ConfigException extends the base PHP Exception class and is thrown when configuration operations + fail. Common scenarios include missing configuration files, invalid file formats, parse errors, + and missing required configuration keys. Used by ConfigFileLoaderStrategy implementations and + can be thrown by ConfigStrategy implementations for validation errors. +questions_answered: + - What is ConfigException? + - When is ConfigException thrown? + - How do I handle configuration errors? + - How do I throw ConfigException? +audience: + - developers + - backend engineers +tags: + - exception + - config + - error-handling +llm_tags: + - config-exception + - error-handling +keywords: + - ConfigException class + - configuration errors + - exception handling +related: + - ../introduction + - ../interfaces/config-file-loader-strategy +see_also: + - ../services/config-service + - ../interfaces/config-strategy +noindex: false +--- + +# ConfigException Class + +The `ConfigException` class is thrown when **configuration operations fail**. It extends PHP's base `Exception` class. + +## Class definition + +```php +namespace PHPNomad\Config\Exceptions; + +class ConfigException extends \Exception {} +``` + +--- + +## When it's thrown + +### Missing configuration file + +```php +class PhpConfigLoader implements ConfigFileLoaderStrategy +{ + public function loadFileConfigs(string $path): array + { + if (!file_exists($path)) { + throw new ConfigException("Config file not found: {$path}"); + } + // ... + } +} +``` + +### Invalid file format + +```php +class JsonConfigLoader implements ConfigFileLoaderStrategy +{ + public function loadFileConfigs(string $path): array + { + $content = file_get_contents($path); + $config = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new ConfigException( + "Invalid JSON in {$path}: " . json_last_error_msg() + ); + } + + return $config; + } +} +``` + +### Invalid return type + +```php +class PhpConfigLoader implements ConfigFileLoaderStrategy +{ + public function loadFileConfigs(string $path): array + { + $config = require $path; + + if (!is_array($config)) { + throw new ConfigException( + "Config file must return an array: {$path}" + ); + } + + return $config; + } +} +``` + +### Missing required configuration + +```php +class ConfigValidator +{ + public function validate(ConfigStrategy $config): void + { + $required = ['database.host', 'app.secret_key']; + + foreach ($required as $key) { + if (!$config->has($key)) { + throw new ConfigException("Missing required config: {$key}"); + } + } + } +} +``` + +--- + +## Handling ConfigException + +### Basic try-catch + +```php +use PHPNomad\Config\Exceptions\ConfigException; + +try { + $service->registerConfig('app', '/path/to/app.php'); +} catch (ConfigException $e) { + error_log("Configuration error: " . $e->getMessage()); + // Handle the error appropriately +} +``` + +### With fallback configuration + +```php +try { + $service->registerConfig('cache', '/config/cache.php'); +} catch (ConfigException $e) { + // Use default cache configuration + $strategy->register('cache', [ + 'driver' => 'array', + 'ttl' => 3600, + ]); + + error_log("Using fallback cache config: " . $e->getMessage()); +} +``` + +### Graceful degradation + +```php +class Application +{ + public function loadOptionalConfigs(): void + { + $optional = ['analytics', 'features', 'experiments']; + + foreach ($optional as $config) { + try { + $this->configService->registerConfig( + $config, + "{$this->configDir}/{$config}.php" + ); + } catch (ConfigException $e) { + // Optional configs can fail silently + $this->logger->debug("Optional config not loaded: {$config}"); + } + } + } +} +``` + +### Fail fast for required configs + +```php +class Application +{ + public function loadRequiredConfigs(): void + { + $required = ['app', 'database']; + + foreach ($required as $config) { + try { + $this->configService->registerConfig( + $config, + "{$this->configDir}/{$config}.php" + ); + } catch (ConfigException $e) { + // Required configs must exist - fail the application + throw new RuntimeException( + "Cannot start application: {$e->getMessage()}", + 0, + $e + ); + } + } + } +} +``` + +--- + +## Creating helpful error messages + +Include context in exception messages: + +```php +// Good: specific and actionable +throw new ConfigException( + "Config file not found: /app/config/database.php. " . + "Ensure the file exists and is readable." +); + +// Good: includes the parse error details +throw new ConfigException(sprintf( + "Failed to parse JSON config '%s' at line %d: %s", + $path, + $lineNumber, + $parseError +)); + +// Bad: vague +throw new ConfigException("Config error"); + +// Bad: missing file path +throw new ConfigException("File not found"); +``` + +--- + +## Custom exception subclasses + +For complex applications, create specific exception types: + +```php +class ConfigFileNotFoundException extends ConfigException +{ + public function __construct(string $path) + { + parent::__construct("Config file not found: {$path}"); + } +} + +class ConfigParseException extends ConfigException +{ + public function __construct(string $path, string $error) + { + parent::__construct("Failed to parse {$path}: {$error}"); + } +} + +class MissingConfigKeyException extends ConfigException +{ + public function __construct(string $key) + { + parent::__construct("Missing required config key: {$key}"); + } +} + +// Usage +try { + $config = $this->loadConfig($path); +} catch (ConfigFileNotFoundException $e) { + // Handle missing file specifically +} catch (ConfigParseException $e) { + // Handle parse errors specifically +} catch (ConfigException $e) { + // Handle other config errors +} +``` + +--- + +## Testing exception scenarios + +```php +class ConfigLoaderTest extends TestCase +{ + public function test_throws_for_missing_file(): void + { + $loader = new PhpConfigLoader(); + + $this->expectException(ConfigException::class); + $this->expectExceptionMessage('not found'); + + $loader->loadFileConfigs('/nonexistent/file.php'); + } + + public function test_throws_for_invalid_json(): void + { + $file = $this->createTempFile('{ invalid json }'); + $loader = new JsonConfigLoader(); + + $this->expectException(ConfigException::class); + $this->expectExceptionMessage('Invalid JSON'); + + $loader->loadFileConfigs($file); + } + + public function test_exception_includes_file_path(): void + { + $loader = new PhpConfigLoader(); + $path = '/specific/path/config.php'; + + try { + $loader->loadFileConfigs($path); + $this->fail('Expected ConfigException'); + } catch (ConfigException $e) { + $this->assertStringContainsString($path, $e->getMessage()); + } + } +} +``` + +--- + +## See also + +- [ConfigFileLoaderStrategy](../interfaces/config-file-loader-strategy) — Where exceptions are typically thrown +- [ConfigService](../services/config-service) — Using the service with error handling +- [ConfigStrategy](../interfaces/config-strategy) — Storage interface that may throw exceptions diff --git a/public/docs/packages/config/interfaces/config-file-loader-strategy.md b/public/docs/packages/config/interfaces/config-file-loader-strategy.md new file mode 100644 index 0000000..c7b01c7 --- /dev/null +++ b/public/docs/packages/config/interfaces/config-file-loader-strategy.md @@ -0,0 +1,408 @@ +--- +id: config-interface-config-file-loader-strategy +slug: docs/packages/config/interfaces/config-file-loader-strategy +title: ConfigFileLoaderStrategy Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The ConfigFileLoaderStrategy interface defines how configuration files are loaded and parsed into arrays. +llm_summary: > + The ConfigFileLoaderStrategy interface enables pluggable file format support for configuration loading. + It defines a single method loadFileConfigs(string $path): array that reads a file and returns its + contents as a PHP array. Implementations exist for PHP arrays, JSON, YAML, and other formats. + Used by ConfigService to load configuration files before registering them with ConfigStrategy. +questions_answered: + - What is ConfigFileLoaderStrategy? + - How do I load configuration from files? + - How do I support custom file formats? + - How do I implement a YAML config loader? +audience: + - developers + - backend engineers +tags: + - interface + - config + - file-loading +llm_tags: + - config-file-loader + - file-parsing + - pluggable-formats +keywords: + - ConfigFileLoaderStrategy interface + - configuration file loading + - file format support +related: + - ../introduction + - ../services/config-service +see_also: + - config-strategy + - ../exceptions/config-exception +noindex: false +--- + +# ConfigFileLoaderStrategy Interface + +The `ConfigFileLoaderStrategy` interface defines how configuration files are **loaded and parsed**. It enables pluggable file format support. + +## Interface definition + +```php +namespace PHPNomad\Config\Interfaces; + +interface ConfigFileLoaderStrategy +{ + /** + * Loads configurations from the specified file. + */ + public function loadFileConfigs(string $path): array; +} +``` + +## Methods + +### `loadFileConfigs(string $path): array` + +Reads a configuration file and returns its contents as an array. + +**Parameters:** +- `$path` — Absolute path to the configuration file + +**Returns:** `array` — The parsed configuration data + +**Throws:** `ConfigException` — If file doesn't exist or parsing fails + +--- + +## PHP array loader + +The simplest loader—PHP files that return arrays: + +```php +use PHPNomad\Config\Interfaces\ConfigFileLoaderStrategy; +use PHPNomad\Config\Exceptions\ConfigException; + +class PhpConfigLoader implements ConfigFileLoaderStrategy +{ + public function loadFileConfigs(string $path): array + { + if (!file_exists($path)) { + throw new ConfigException("Config file not found: {$path}"); + } + + $config = require $path; + + if (!is_array($config)) { + throw new ConfigException("Config file must return an array: {$path}"); + } + + return $config; + } +} +``` + +Configuration file: + +```php + 'mysql', + 'host' => $_ENV['DB_HOST'] ?? 'localhost', + 'port' => (int)($_ENV['DB_PORT'] ?? 3306), + 'credentials' => [ + 'username' => $_ENV['DB_USER'] ?? 'root', + 'password' => $_ENV['DB_PASS'] ?? '', + ], +]; +``` + +**Advantages of PHP config files:** +- Can include PHP logic and environment variable access +- No parsing overhead +- IDE autocompletion works + +--- + +## JSON loader + +```php +class JsonConfigLoader implements ConfigFileLoaderStrategy +{ + public function loadFileConfigs(string $path): array + { + if (!file_exists($path)) { + throw new ConfigException("Config file not found: {$path}"); + } + + $content = file_get_contents($path); + $config = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new ConfigException( + "Invalid JSON in {$path}: " . json_last_error_msg() + ); + } + + return $config; + } +} +``` + +Configuration file: + +```json +{ + "driver": "mysql", + "host": "localhost", + "port": 3306, + "credentials": { + "username": "app_user", + "password": "secret" + } +} +``` + +**Note:** For production JSON config support, see [json-config-integration](/packages/json-config-integration/introduction). + +--- + +## YAML loader + +```php +use Symfony\Component\Yaml\Yaml; + +class YamlConfigLoader implements ConfigFileLoaderStrategy +{ + public function loadFileConfigs(string $path): array + { + if (!file_exists($path)) { + throw new ConfigException("Config file not found: {$path}"); + } + + try { + return Yaml::parseFile($path); + } catch (ParseException $e) { + throw new ConfigException( + "Invalid YAML in {$path}: " . $e->getMessage() + ); + } + } +} +``` + +Configuration file: + +```yaml +driver: mysql +host: localhost +port: 3306 +credentials: + username: app_user + password: secret +``` + +--- + +## With environment variable expansion + +Expand `${VAR}` placeholders in config files: + +```php +class EnvExpandingJsonLoader implements ConfigFileLoaderStrategy +{ + public function loadFileConfigs(string $path): array + { + if (!file_exists($path)) { + throw new ConfigException("Config file not found: {$path}"); + } + + $content = file_get_contents($path); + + // Expand ${VAR} and ${VAR:-default} syntax + $content = preg_replace_callback( + '/\$\{([A-Z_]+)(?::-([^}]*))?\}/', + function ($matches) { + $value = getenv($matches[1]); + if ($value === false) { + return $matches[2] ?? ''; + } + return $value; + }, + $content + ); + + $config = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new ConfigException( + "Invalid JSON in {$path}: " . json_last_error_msg() + ); + } + + return $config; + } +} +``` + +Configuration file: + +```json +{ + "host": "${DB_HOST:-localhost}", + "credentials": { + "username": "${DB_USER}", + "password": "${DB_PASS}" + } +} +``` + +--- + +## Composite loader + +Support multiple file formats with a single loader: + +```php +class CompositeConfigLoader implements ConfigFileLoaderStrategy +{ + private array $loaders = []; + + public function registerLoader(string $extension, ConfigFileLoaderStrategy $loader): void + { + $this->loaders[$extension] = $loader; + } + + public function loadFileConfigs(string $path): array + { + $extension = pathinfo($path, PATHINFO_EXTENSION); + + if (!isset($this->loaders[$extension])) { + throw new ConfigException( + "No loader registered for .{$extension} files" + ); + } + + return $this->loaders[$extension]->loadFileConfigs($path); + } +} + +// Usage +$loader = new CompositeConfigLoader(); +$loader->registerLoader('php', new PhpConfigLoader()); +$loader->registerLoader('json', new JsonConfigLoader()); +$loader->registerLoader('yaml', new YamlConfigLoader()); + +// Automatically uses correct loader based on extension +$loader->loadFileConfigs('/config/database.json'); +$loader->loadFileConfigs('/config/cache.yaml'); +$loader->loadFileConfigs('/config/app.php'); +``` + +--- + +## Best practices + +### Validate file existence early + +```php +public function loadFileConfigs(string $path): array +{ + if (!file_exists($path)) { + throw new ConfigException("Config file not found: {$path}"); + } + + if (!is_readable($path)) { + throw new ConfigException("Config file not readable: {$path}"); + } + + // ... load and parse +} +``` + +### Provide clear error messages + +```php +if (json_last_error() !== JSON_ERROR_NONE) { + throw new ConfigException(sprintf( + "Failed to parse JSON config file '%s': %s", + $path, + json_last_error_msg() + )); +} +``` + +### Handle encoding issues + +```php +$content = file_get_contents($path); + +// Ensure UTF-8 +if (!mb_check_encoding($content, 'UTF-8')) { + $content = mb_convert_encoding($content, 'UTF-8', 'auto'); +} +``` + +--- + +## Testing + +```php +class PhpConfigLoaderTest extends TestCase +{ + private string $tempDir; + + protected function setUp(): void + { + $this->tempDir = sys_get_temp_dir() . '/config_test_' . uniqid(); + mkdir($this->tempDir); + } + + protected function tearDown(): void + { + array_map('unlink', glob($this->tempDir . '/*')); + rmdir($this->tempDir); + } + + public function test_loads_php_array_file(): void + { + $file = $this->tempDir . '/test.php'; + file_put_contents($file, ' "value"];'); + + $loader = new PhpConfigLoader(); + $config = $loader->loadFileConfigs($file); + + $this->assertEquals(['key' => 'value'], $config); + } + + public function test_throws_for_missing_file(): void + { + $loader = new PhpConfigLoader(); + + $this->expectException(ConfigException::class); + $loader->loadFileConfigs('/nonexistent/file.php'); + } + + public function test_throws_for_non_array_return(): void + { + $file = $this->tempDir . '/test.php'; + file_put_contents($file, 'expectException(ConfigException::class); + $loader->loadFileConfigs($file); + } +} +``` + +--- + +## See also + +- [ConfigStrategy](config-strategy) — Where loaded configuration is stored +- [ConfigService](../services/config-service) — Orchestrates loading and registration +- [json-config-integration](/packages/json-config-integration/introduction) — Production JSON loader diff --git a/public/docs/packages/config/interfaces/config-strategy.md b/public/docs/packages/config/interfaces/config-strategy.md new file mode 100644 index 0000000..7d45b62 --- /dev/null +++ b/public/docs/packages/config/interfaces/config-strategy.md @@ -0,0 +1,421 @@ +--- +id: config-interface-config-strategy +slug: docs/packages/config/interfaces/config-strategy +title: ConfigStrategy Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The ConfigStrategy interface defines how configuration data is stored, retrieved, and checked using dot-notation keys. +llm_summary: > + The ConfigStrategy interface is the main contract for configuration management in phpnomad/config. + It defines three methods: register(string $key, array $configData) for storing configuration sections, + has(string $key) for checking existence, and get(string $key, $default) for retrieval. Supports + dot-notation for nested access like 'database.credentials.username'. Implementations can use + in-memory arrays, databases, Redis, or any storage backend. +questions_answered: + - What is ConfigStrategy? + - How do I store configuration data? + - How do I retrieve configuration with dot-notation? + - How do I implement ConfigStrategy? + - What storage backends can I use? +audience: + - developers + - backend engineers +tags: + - interface + - config + - strategy +llm_tags: + - config-strategy + - dot-notation + - configuration-storage +keywords: + - ConfigStrategy interface + - configuration storage + - dot notation + - nested configuration +related: + - ../introduction + - ../services/config-service +see_also: + - config-file-loader-strategy + - ../exceptions/config-exception +noindex: false +--- + +# ConfigStrategy Interface + +The `ConfigStrategy` interface defines how configuration data is **stored and retrieved**. It's the main interface applications interact with for configuration access. + +## Interface definition + +```php +namespace PHPNomad\Config\Interfaces; + +interface ConfigStrategy +{ + /** + * Registers a top-level set of configuration data. + */ + public function register(string $key, array $configData); + + /** + * Checks if a configuration key exists. + */ + public function has(string $key): bool; + + /** + * Gets a configuration value by dot-notated key. + */ + public function get(string $key, $default = null); +} +``` + +## Methods + +### `register(string $key, array $configData): static` + +Stores a section of configuration under a namespace. + +**Parameters:** +- `$key` — The top-level namespace (e.g., `'database'`, `'cache'`) +- `$configData` — The configuration array to store + +**Returns:** `static` — For fluent chaining + +**Example:** +```php +$config->register('database', [ + 'host' => 'localhost', + 'port' => 3306, + 'credentials' => [ + 'username' => 'app_user', + 'password' => 'secret' + ] +]); +``` + +### `has(string $key): bool` + +Checks if a configuration key exists. + +**Parameters:** +- `$key` — Dot-notated key to check + +**Returns:** `bool` — True if key exists, false otherwise + +**Example:** +```php +$config->has('database.host'); // true +$config->has('database.credentials.username'); // true +$config->has('database.nonexistent'); // false +``` + +### `get(string $key, $default = null): mixed` + +Retrieves a configuration value by dot-notated key. + +**Parameters:** +- `$key` — Dot-notated key (e.g., `'database.credentials.username'`) +- `$default` — Value to return if key doesn't exist + +**Returns:** The configuration value, or default if not found + +**Example:** +```php +$config->get('database.host'); // 'localhost' +$config->get('database.timeout', 30); // 30 (default) +$config->get('database.credentials'); // ['username' => '...', 'password' => '...'] +``` + +--- + +## Dot-notation access + +Dot-notation lets you access nested configuration: + +```php +$config->register('app', [ + 'name' => 'MyApp', + 'database' => [ + 'primary' => [ + 'host' => 'db1.example.com', + 'port' => 3306 + ], + 'replica' => [ + 'host' => 'db2.example.com', + 'port' => 3306 + ] + ] +]); + +// Access nested values +$config->get('app.name'); // 'MyApp' +$config->get('app.database.primary.host'); // 'db1.example.com' +$config->get('app.database.replica'); // ['host' => '...', 'port' => 3306] +``` + +--- + +## Basic implementation + +Here's a complete in-memory implementation: + +```php +use PHPNomad\Config\Interfaces\ConfigStrategy; + +class ArrayConfig implements ConfigStrategy +{ + private array $data = []; + + public function register(string $key, array $configData): static + { + $this->data[$key] = $configData; + return $this; + } + + public function has(string $key): bool + { + return $this->resolve($key) !== null; + } + + public function get(string $key, $default = null) + { + return $this->resolve($key) ?? $default; + } + + private function resolve(string $key): mixed + { + $parts = explode('.', $key); + $value = $this->data; + + foreach ($parts as $part) { + if (!is_array($value) || !array_key_exists($part, $value)) { + return null; + } + $value = $value[$part]; + } + + return $value; + } +} +``` + +--- + +## With merge support + +Allow configuration overrides by merging: + +```php +class MergeableConfig implements ConfigStrategy +{ + private array $data = []; + + public function register(string $key, array $configData): static + { + if (isset($this->data[$key])) { + // Deep merge with existing + $this->data[$key] = $this->deepMerge( + $this->data[$key], + $configData + ); + } else { + $this->data[$key] = $configData; + } + return $this; + } + + private function deepMerge(array $base, array $override): array + { + foreach ($override as $key => $value) { + if (is_array($value) && isset($base[$key]) && is_array($base[$key])) { + $base[$key] = $this->deepMerge($base[$key], $value); + } else { + $base[$key] = $value; + } + } + return $base; + } + + // ... has() and get() same as ArrayConfig +} +``` + +Usage for environment overrides: + +```php +// Base configuration +$config->register('database', [ + 'host' => 'localhost', + 'port' => 3306, + 'debug' => false +]); + +// Production override (merges) +$config->register('database', [ + 'host' => 'db.production.com', + 'debug' => false +]); + +$config->get('database.host'); // 'db.production.com' +$config->get('database.port'); // 3306 (preserved from base) +``` + +--- + +## Alternative backends + +### Environment-based + +```php +class EnvConfig implements ConfigStrategy +{ + private array $prefixes = []; + + public function register(string $key, array $configData): static + { + // Store prefix mapping for environment variable lookup + $this->prefixes[$key] = strtoupper($key); + return $this; + } + + public function has(string $key): bool + { + return getenv($this->toEnvKey($key)) !== false; + } + + public function get(string $key, $default = null) + { + $value = getenv($this->toEnvKey($key)); + return $value !== false ? $value : $default; + } + + private function toEnvKey(string $key): string + { + // database.host -> DATABASE_HOST + return strtoupper(str_replace('.', '_', $key)); + } +} +``` + +### Cached + +```php +class CachedConfig implements ConfigStrategy +{ + private array $cache = []; + + public function __construct( + private ConfigStrategy $inner, + private CacheInterface $cacheBackend + ) {} + + public function get(string $key, $default = null) + { + if (!isset($this->cache[$key])) { + $this->cache[$key] = $this->cacheBackend->get( + "config:{$key}", + fn() => $this->inner->get($key, $default) + ); + } + return $this->cache[$key]; + } + + // ... other methods delegate to $inner +} +``` + +--- + +## Best practices + +### Use clear namespaces + +```php +// Good: organized by domain +$config->register('database', [...]); +$config->register('cache', [...]); +$config->register('mail', [...]); + +// Bad: everything flat +$config->register('settings', [ + 'db_host' => '...', + 'cache_ttl' => '...', + 'mail_from' => '...' +]); +``` + +### Provide sensible defaults + +```php +// In your application code +$timeout = $config->get('api.timeout', 30); +$retries = $config->get('api.retries', 3); +``` + +### Validate early + +Check required configuration at bootstrap: + +```php +$required = ['database.host', 'app.secret_key']; +foreach ($required as $key) { + if (!$config->has($key)) { + throw new ConfigException("Missing: {$key}"); + } +} +``` + +--- + +## Testing + +```php +class ArrayConfigTest extends TestCase +{ + public function test_registers_and_retrieves_config(): void + { + $config = new ArrayConfig(); + $config->register('app', ['name' => 'Test']); + + $this->assertEquals('Test', $config->get('app.name')); + } + + public function test_returns_default_for_missing_key(): void + { + $config = new ArrayConfig(); + + $this->assertEquals('default', $config->get('missing.key', 'default')); + } + + public function test_has_returns_true_for_existing_key(): void + { + $config = new ArrayConfig(); + $config->register('app', ['debug' => true]); + + $this->assertTrue($config->has('app.debug')); + $this->assertFalse($config->has('app.nonexistent')); + } + + public function test_supports_deep_nesting(): void + { + $config = new ArrayConfig(); + $config->register('a', ['b' => ['c' => ['d' => 'value']]]); + + $this->assertEquals('value', $config->get('a.b.c.d')); + } +} +``` + +--- + +## See also + +- [ConfigFileLoaderStrategy](config-file-loader-strategy) — Loading configuration from files +- [ConfigService](../services/config-service) — Orchestrating file loading and registration +- [ConfigException](../exceptions/config-exception) — Error handling diff --git a/public/docs/packages/config/interfaces/introduction.md b/public/docs/packages/config/interfaces/introduction.md new file mode 100644 index 0000000..48aef37 --- /dev/null +++ b/public/docs/packages/config/interfaces/introduction.md @@ -0,0 +1,112 @@ +--- +id: config-interfaces-introduction +slug: docs/packages/config/interfaces/introduction +title: Config Interfaces Overview +doc_type: explanation +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: Overview of the two interfaces provided by the config package for configuration management. +llm_summary: > + The config package provides two interfaces: ConfigStrategy for storing and retrieving configuration + data with dot-notation access, and ConfigFileLoaderStrategy for loading configuration from files. + ConfigStrategy is the main interface applications interact with, while ConfigFileLoaderStrategy + enables pluggable file format support (PHP arrays, JSON, YAML, etc.). +questions_answered: + - What interfaces does the config package provide? + - How do the config interfaces relate to each other? + - Which interface should I implement? +audience: + - developers + - backend engineers +tags: + - interfaces + - config + - configuration +llm_tags: + - interface-overview + - config-interfaces +keywords: + - config interfaces + - ConfigStrategy + - ConfigFileLoaderStrategy +related: + - ../introduction +see_also: + - config-strategy + - config-file-loader-strategy + - ../services/config-service +noindex: false +--- + +# Config Interfaces + +The config package provides two interfaces that separate configuration storage from file loading: + +| Interface | Purpose | When to Implement | +|-----------|---------|-------------------| +| [ConfigStrategy](config-strategy) | Store and retrieve configuration with dot-notation | Custom storage backends (database, Redis, etc.) | +| [ConfigFileLoaderStrategy](config-file-loader-strategy) | Load configuration from files | Custom file formats (YAML, TOML, etc.) | + +--- + +## How the interfaces relate + +``` +┌──────────────────────────────────┐ +│ Config Files │ +│ (PHP, JSON, YAML, etc.) │ +└─────────────┬────────────────────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ ConfigFileLoaderStrategy │ +│ loadFileConfigs($path) │ +│ → reads file, returns array │ +└─────────────┬────────────────────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ ConfigService │ +│ (orchestrates the flow) │ +└─────────────┬────────────────────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ ConfigStrategy │ +│ register($key, $data) │ +│ get($key) / has($key) │ +└──────────────────────────────────┘ + │ + ▼ + Application +``` + +--- + +## Choosing what to implement + +**Implement ConfigStrategy** when you need: +- A custom storage backend (database, Redis, environment variables) +- Caching or lazy-loading behavior +- Validation on configuration access + +**Implement ConfigFileLoaderStrategy** when you need: +- Support for a new file format (YAML, TOML, INI) +- Custom parsing logic (environment variable expansion, etc.) +- Encrypted configuration files + +**Use existing implementations** when: +- In-memory storage works for your needs +- JSON or PHP array files are sufficient + +--- + +## Next steps + +- [ConfigStrategy](config-strategy) — Configuration storage and retrieval +- [ConfigFileLoaderStrategy](config-file-loader-strategy) — File format loading +- [ConfigService](../services/config-service) — Orchestration service diff --git a/public/docs/packages/config/introduction.md b/public/docs/packages/config/introduction.md new file mode 100644 index 0000000..3289db2 --- /dev/null +++ b/public/docs/packages/config/introduction.md @@ -0,0 +1,327 @@ +--- +id: config-introduction +slug: docs/packages/config/introduction +title: Config Package +doc_type: explanation +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The config package provides a strategy-based configuration management system with dot-notation access and pluggable file loaders. +llm_summary: > + phpnomad/config provides a flexible configuration management system using the strategy pattern. + ConfigStrategy interface defines how configuration is stored and accessed (with dot-notation support). + ConfigFileLoaderStrategy interface defines how config files are loaded from disk (supporting PHP, JSON, + YAML, etc.). ConfigService orchestrates loading files and registering them with the strategy. The package + has zero dependencies and is used by json-config-integration for JSON file support and wordpress-plugin + for WordPress configuration. Supports nested configuration with dot-notation access like 'database.host'. +questions_answered: + - What is the config package? + - How do I manage configuration in PHPNomad? + - How do I access nested configuration values? + - What is dot-notation configuration access? + - How do I load configuration from files? + - How do I create a custom configuration loader? + - How do I implement ConfigStrategy? + - What packages use the config system? +audience: + - developers + - backend engineers +tags: + - config + - configuration + - strategy-pattern + - settings +llm_tags: + - configuration-management + - dot-notation + - strategy-pattern + - file-loading +keywords: + - phpnomad config + - configuration management php + - ConfigStrategy + - dot notation config + - config file loader +related: + - ../json-config-integration/introduction + - ../di/introduction +see_also: + - interfaces/introduction + - services/introduction + - ../core/introduction +noindex: false +--- + +# Config + +`phpnomad/config` provides a **strategy-based configuration management system** for PHP applications. Instead of hardcoding how configuration is stored or loaded, the package uses interfaces that let you: + +* **Choose your storage** — In-memory, database, Redis, or custom backends +* **Choose your file format** — PHP arrays, JSON, YAML, or any format you need +* **Access nested values** — Use dot-notation like `database.credentials.username` +* **Swap implementations** — Change strategies without touching application code + +--- + +## Key ideas at a glance + +| Component | Purpose | Documentation | +|-----------|---------|---------------| +| **ConfigStrategy** | Interface for storing and retrieving configuration data | [Interface docs](interfaces/config-strategy) | +| **ConfigFileLoaderStrategy** | Interface for loading configuration from files | [Interface docs](interfaces/config-file-loader-strategy) | +| **ConfigService** | Coordinates file loading and strategy registration | [Service docs](services/config-service) | +| **ConfigException** | Thrown when configuration operations fail | [Exception docs](exceptions/config-exception) | + +--- + +## Why this package exists + +Configuration management seems simple until you need to: + +* **Support multiple environments** — Development, staging, production +* **Change file formats** — Move from PHP arrays to JSON or YAML +* **Test configuration** — Mock configuration without file system access +* **Share configuration** — Let packages register their own config sections + +Without abstraction, you end up with: + +| Problem | What happens | +|---------|--------------| +| Hardcoded file loading | Can't switch from PHP to JSON without rewriting code | +| Global arrays | Hard to test, easy to corrupt, no type safety | +| Scattered `$_ENV` calls | Configuration access spread throughout codebase | +| No namespacing | Package configs collide with application configs | + +This package provides **clean interfaces** that separate: + +* **What** configuration you need (the keys) +* **Where** it's stored (the strategy) +* **How** it's loaded (the file loader) + +--- + +## Installation + +```bash +composer require phpnomad/config +``` + +**Requirements:** PHP 7.4+ + +**Dependencies:** None (zero dependencies) + +--- + +## The configuration flow + +When loading configuration through `ConfigService`: + +``` +Config file on disk (PHP, JSON, etc.) + │ + ▼ +┌─────────────────────────────────┐ +│ ConfigFileLoaderStrategy │ +│ loadFileConfigs($path) │ +│ → reads file, returns array │ +└─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ ConfigService │ +│ registerConfig($key, $path) │ +│ → coordinates loading │ +└─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ ConfigStrategy │ +│ register($key, $data) │ +│ → stores under namespace │ +└─────────────────────────────────┘ + │ + ▼ +Application calls $config->get('key.nested.value') +``` + +Each component has a single responsibility: +* **Loader** — Knows how to read a file format +* **Service** — Orchestrates the process +* **Strategy** — Stores and retrieves data + +--- + +## Quick example + +```php +use PHPNomad\Config\Interfaces\ConfigStrategy; +use PHPNomad\Config\Services\ConfigService; + +// 1. Create a strategy (in-memory storage) +class ArrayConfig implements ConfigStrategy +{ + private array $data = []; + + public function register(string $key, array $configData): static + { + $this->data[$key] = $configData; + return $this; + } + + public function has(string $key): bool + { + return $this->resolve($key) !== null; + } + + public function get(string $key, $default = null) + { + return $this->resolve($key) ?? $default; + } + + private function resolve(string $key): mixed + { + $parts = explode('.', $key); + $value = $this->data; + foreach ($parts as $part) { + if (!is_array($value) || !array_key_exists($part, $value)) { + return null; + } + $value = $value[$part]; + } + return $value; + } +} + +// 2. Register configuration +$config = new ArrayConfig(); +$config->register('database', [ + 'host' => 'localhost', + 'port' => 3306, + 'credentials' => [ + 'username' => 'app_user', + 'password' => 'secret123' + ] +]); + +// 3. Access with dot-notation +$config->get('database.host'); // 'localhost' +$config->get('database.credentials.username'); // 'app_user' +$config->get('database.timeout', 30); // 30 (default) +``` + +--- + +## When to use this package + +The config package is appropriate when: + +| Scenario | Why it helps | +|----------|--------------| +| Multi-environment apps | Different strategies for dev/staging/prod | +| Plugin systems | Packages register their own config sections | +| Testing | Mock ConfigStrategy for predictable tests | +| Format flexibility | Switch file formats without code changes | +| Centralized access | One place for all configuration | + +### Common use cases + +* **Application settings** — Debug mode, timezone, locale +* **Database connections** — Host, port, credentials +* **External services** — API keys, endpoints, timeouts +* **Feature flags** — Enable/disable functionality +* **Cache settings** — Driver, TTL, prefix + +--- + +## When NOT to use this package + +### Simple scripts + +If you have a single-file script, just use an array: + +```php +$config = ['api_key' => 'xxx', 'timeout' => 30]; +``` + +### Environment-only configuration + +If you only need environment variables: + +```php +$dbHost = getenv('DATABASE_HOST'); +``` + +### No file-based configuration + +If all configuration comes from a database or API, you don't need `ConfigFileLoaderStrategy`—just implement `ConfigStrategy` with your storage backend. + +--- + +## Best practices + +1. **Use namespaced keys** — Register each domain under a clear namespace +2. **Type your configuration access** — Wrap access in typed methods +3. **Validate configuration early** — Check required keys at bootstrap +4. **Use environment variables for secrets** — Don't hardcode credentials + +See the individual component docs for detailed best practices: +- [ConfigStrategy best practices](interfaces/config-strategy#best-practices) +- [ConfigFileLoaderStrategy best practices](interfaces/config-file-loader-strategy#best-practices) +- [ConfigService best practices](services/config-service#best-practices) + +--- + +## Relationship to other packages + +### Packages that depend on config + +| Package | How it uses config | +|---------|-------------------| +| [json-config-integration](/packages/json-config-integration/introduction) | Provides JSON file loader implementation | +| [wordpress-plugin](/packages/wordpress-plugin/introduction) | Configuration management for WordPress plugins | + +### Related packages + +| Package | Relationship | +|---------|-------------| +| [di](/packages/di/introduction) | DI container can inject ConfigStrategy | +| [loader](/packages/loader/introduction) | Loader can load configuration modules | + +--- + +## Package contents + +### Interfaces + +| Interface | Purpose | +|-----------|---------| +| [ConfigStrategy](interfaces/config-strategy) | Configuration storage and retrieval with dot-notation | +| [ConfigFileLoaderStrategy](interfaces/config-file-loader-strategy) | File format loading | + +[View all interfaces →](interfaces/introduction) + +### Services + +| Class | Purpose | +|-------|---------| +| [ConfigService](services/config-service) | Orchestrates file loading and registration | + +[View all services →](services/introduction) + +### Exceptions + +| Class | Purpose | +|-------|---------| +| [ConfigException](exceptions/config-exception) | Configuration operation failures | + +--- + +## Next steps + +* **Need JSON config files?** See [JSON Config Integration](/packages/json-config-integration/introduction) +* **Implementing a strategy?** Read [ConfigStrategy interface](interfaces/config-strategy) +* **Loading configuration?** Check [ConfigService](services/config-service) +* **Building a module system?** See [Loader](/packages/loader/introduction) for loading configuration modules diff --git a/public/docs/packages/config/services/config-service.md b/public/docs/packages/config/services/config-service.md new file mode 100644 index 0000000..0c4d9a2 --- /dev/null +++ b/public/docs/packages/config/services/config-service.md @@ -0,0 +1,367 @@ +--- +id: config-service-config-service +slug: docs/packages/config/services/config-service +title: ConfigService Class +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The ConfigService class orchestrates loading configuration files and registering them with a strategy. +llm_summary: > + ConfigService is the orchestration class that connects ConfigFileLoaderStrategy and ConfigStrategy. + It takes both as constructor dependencies and provides registerConfig(string $key, string $path) + to load a file and register it under a namespace. The method returns $this for fluent chaining. + Simplifies the common pattern of loading multiple configuration files at application bootstrap. +questions_answered: + - What is ConfigService? + - How do I load configuration files? + - How do I use ConfigService with dependency injection? + - How do I load multiple configuration files? +audience: + - developers + - backend engineers +tags: + - service + - config + - orchestration +llm_tags: + - config-service + - file-loading + - orchestration +keywords: + - ConfigService class + - configuration orchestration + - file registration +related: + - ../introduction + - ../interfaces/config-strategy +see_also: + - ../interfaces/config-file-loader-strategy + - ../exceptions/config-exception +noindex: false +--- + +# ConfigService Class + +The `ConfigService` class **orchestrates** loading configuration files and registering them with a strategy. It connects the file loader and storage backend. + +## Class definition + +```php +namespace PHPNomad\Config\Services; + +use PHPNomad\Config\Interfaces\ConfigStrategy; +use PHPNomad\Config\Interfaces\ConfigFileLoaderStrategy; + +class ConfigService +{ + public function __construct( + protected ConfigStrategy $configStrategy, + protected ConfigFileLoaderStrategy $configFileLoaderStrategy + ) {} + + public function registerConfig(string $key, string $path): static + { + $configs = $this->configFileLoaderStrategy->loadFileConfigs($path); + $this->configStrategy->register($key, $configs); + return $this; + } +} +``` + +## Constructor + +### `__construct(ConfigStrategy $configStrategy, ConfigFileLoaderStrategy $configFileLoaderStrategy)` + +**Parameters:** +- `$configStrategy` — The storage backend for configuration data +- `$configFileLoaderStrategy` — The file format loader + +## Methods + +### `registerConfig(string $key, string $path): static` + +Loads a configuration file and registers it under a namespace. + +**Parameters:** +- `$key` — The namespace to register under (e.g., `'database'`) +- `$path` — Absolute path to the configuration file + +**Returns:** `static` — For fluent chaining + +**Throws:** `ConfigException` — If file loading fails + +--- + +## Basic usage + +```php +use PHPNomad\Config\Services\ConfigService; + +// Create dependencies +$strategy = new ArrayConfig(); +$loader = new PhpConfigLoader(); + +// Create service +$service = new ConfigService($strategy, $loader); + +// Load a configuration file +$service->registerConfig('database', '/app/config/database.php'); + +// Access via strategy +$host = $strategy->get('database.host'); +``` + +--- + +## Fluent chaining + +Load multiple files with chained calls: + +```php +$service + ->registerConfig('app', __DIR__ . '/config/app.php') + ->registerConfig('database', __DIR__ . '/config/database.php') + ->registerConfig('cache', __DIR__ . '/config/cache.php') + ->registerConfig('mail', __DIR__ . '/config/mail.php') + ->registerConfig('queue', __DIR__ . '/config/queue.php'); +``` + +--- + +## With dependency injection + +Register in your DI container: + +```php +// Registration +$container->set(ConfigStrategy::class, fn() => new ArrayConfig()); + +$container->set(ConfigFileLoaderStrategy::class, fn() => new PhpConfigLoader()); + +$container->set(ConfigService::class, fn($c) => new ConfigService( + $c->get(ConfigStrategy::class), + $c->get(ConfigFileLoaderStrategy::class) +)); + +// Usage +$configService = $container->get(ConfigService::class); +$configService->registerConfig('app', '/config/app.php'); +``` + +--- + +## Environment-aware loading + +Load base configuration plus environment overrides: + +```php +class ConfigLoader +{ + public function __construct( + private ConfigService $service, + private string $configDir, + private string $environment + ) {} + + public function loadAll(): void + { + $files = ['app', 'database', 'cache', 'mail']; + + foreach ($files as $file) { + // Load base config + $basePath = "{$this->configDir}/{$file}.php"; + if (file_exists($basePath)) { + $this->service->registerConfig($file, $basePath); + } + + // Load environment override + $envPath = "{$this->configDir}/{$this->environment}/{$file}.php"; + if (file_exists($envPath)) { + $this->service->registerConfig($file, $envPath); + } + } + } +} + +// Usage +$loader = new ConfigLoader( + $configService, + __DIR__ . '/config', + $_ENV['APP_ENV'] ?? 'production' +); +$loader->loadAll(); +``` + +Directory structure: + +``` +config/ +├── app.php # Base configuration +├── database.php +├── development/ +│ ├── app.php # Development overrides +│ └── database.php +└── production/ + └── database.php # Production overrides +``` + +--- + +## Error handling + +```php +use PHPNomad\Config\Exceptions\ConfigException; + +try { + $service->registerConfig('app', '/path/to/app.php'); +} catch (ConfigException $e) { + // Log the error + error_log("Configuration error: " . $e->getMessage()); + + // Use fallback configuration + $strategy->register('app', [ + 'name' => 'Default App', + 'debug' => false, + ]); +} +``` + +--- + +## Conditional loading + +Load configuration based on conditions: + +```php +class ConditionalConfigLoader +{ + public function __construct( + private ConfigService $service, + private string $configDir + ) {} + + public function load(): void + { + // Always load core config + $this->service->registerConfig('app', "{$this->configDir}/app.php"); + + // Load database config only if needed + if ($this->needsDatabase()) { + $this->service->registerConfig('database', "{$this->configDir}/database.php"); + } + + // Load cache config only if cache is enabled + if (getenv('CACHE_ENABLED') === 'true') { + $this->service->registerConfig('cache', "{$this->configDir}/cache.php"); + } + } + + private function needsDatabase(): bool + { + return getenv('DATABASE_URL') !== false; + } +} +``` + +--- + +## Best practices + +### Load at bootstrap + +Load all configuration early in your application lifecycle: + +```php +// In bootstrap.php or Application::__construct() +$configService + ->registerConfig('app', $configDir . '/app.php') + ->registerConfig('database', $configDir . '/database.php'); + +// Configuration is now available throughout the application +``` + +### Use absolute paths + +```php +// Good: absolute path +$service->registerConfig('app', __DIR__ . '/config/app.php'); + +// Bad: relative path (depends on working directory) +$service->registerConfig('app', 'config/app.php'); +``` + +### Keep the service internal + +Applications should access configuration via `ConfigStrategy`, not `ConfigService`: + +```php +// Good: inject the strategy for reading +class MyService +{ + public function __construct(private ConfigStrategy $config) {} + + public function doSomething(): void + { + $timeout = $this->config->get('api.timeout', 30); + } +} + +// Bad: inject the service just to read config +class MyService +{ + public function __construct(private ConfigService $service) {} + // ConfigService is for loading, not reading +} +``` + +--- + +## Testing + +```php +class ConfigServiceTest extends TestCase +{ + public function test_loads_and_registers_config(): void + { + $strategy = $this->createMock(ConfigStrategy::class); + $loader = $this->createMock(ConfigFileLoaderStrategy::class); + + $loader->expects($this->once()) + ->method('loadFileConfigs') + ->with('/path/to/config.php') + ->willReturn(['key' => 'value']); + + $strategy->expects($this->once()) + ->method('register') + ->with('app', ['key' => 'value']); + + $service = new ConfigService($strategy, $loader); + $service->registerConfig('app', '/path/to/config.php'); + } + + public function test_returns_self_for_chaining(): void + { + $strategy = new ArrayConfig(); + $loader = $this->createMock(ConfigFileLoaderStrategy::class); + $loader->method('loadFileConfigs')->willReturn([]); + + $service = new ConfigService($strategy, $loader); + + $result = $service->registerConfig('app', '/path'); + + $this->assertSame($service, $result); + } +} +``` + +--- + +## See also + +- [ConfigStrategy](../interfaces/config-strategy) — The storage interface +- [ConfigFileLoaderStrategy](../interfaces/config-file-loader-strategy) — The file loader interface +- [ConfigException](../exceptions/config-exception) — Error handling diff --git a/public/docs/packages/config/services/introduction.md b/public/docs/packages/config/services/introduction.md new file mode 100644 index 0000000..4f3b37c --- /dev/null +++ b/public/docs/packages/config/services/introduction.md @@ -0,0 +1,98 @@ +--- +id: config-services-introduction +slug: docs/packages/config/services/introduction +title: Config Services Overview +doc_type: explanation +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: Overview of the ConfigService class that orchestrates configuration file loading and registration. +llm_summary: > + The config package provides one service class: ConfigService. This service orchestrates the workflow + of loading configuration files via ConfigFileLoaderStrategy and registering them with ConfigStrategy. + It provides a fluent interface for loading multiple configuration files in sequence. +questions_answered: + - What services does the config package provide? + - How does ConfigService work? + - How do I load multiple configuration files? +audience: + - developers + - backend engineers +tags: + - services + - config + - orchestration +llm_tags: + - service-overview + - config-service +keywords: + - ConfigService + - configuration loading +related: + - ../introduction +see_also: + - config-service + - ../interfaces/config-strategy + - ../interfaces/config-file-loader-strategy +noindex: false +--- + +# Config Services + +The config package provides one service class that orchestrates configuration loading: + +| Service | Purpose | +|---------|---------| +| [ConfigService](config-service) | Coordinates file loading and strategy registration | + +--- + +## The orchestration pattern + +`ConfigService` connects the loader and strategy: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ConfigService │ +│ │ +│ registerConfig('database', '/path/to/database.php') │ +│ │ │ +│ ├──→ ConfigFileLoaderStrategy::loadFileConfigs() │ +│ │ (reads file, returns array) │ +│ │ │ +│ └──→ ConfigStrategy::register('database', $data) │ +│ (stores under namespace) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Quick example + +```php +use PHPNomad\Config\Services\ConfigService; + +$strategy = new ArrayConfig(); +$loader = new PhpConfigLoader(); +$service = new ConfigService($strategy, $loader); + +// Load multiple config files +$service + ->registerConfig('database', __DIR__ . '/config/database.php') + ->registerConfig('cache', __DIR__ . '/config/cache.php') + ->registerConfig('mail', __DIR__ . '/config/mail.php'); + +// Access via strategy +$dbHost = $strategy->get('database.host'); +``` + +--- + +## Next steps + +- [ConfigService](config-service) — Full service documentation +- [ConfigStrategy](../interfaces/config-strategy) — Where configuration is stored +- [ConfigFileLoaderStrategy](../interfaces/config-file-loader-strategy) — How files are loaded