diff --git a/README.md b/README.md index 2a8fb0f..7d3495a 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,8 @@ real-world PHP projects. | Class | Description | |---------------------|---------------------------------------------------------------------------------------------------------------------| -| **Config** | Dot-access configuration loader. | +| **Config** | Dot-access configuration loader with explicit hook-aware variants (`getWithHooks`, `setWithHooks`, `fillWithHooks`). | | **LazyFileConfig** | First-segment lazy loader (`db.host` loads `db.php` on demand) for lower memory usage on large config trees. | -| **DynamicConfig** | Extends `Config` with **on-get/on-set hooks** to transform values dynamically (e.g., encrypt/decrypt, auto-format). | | **BaseConfigTrait** | Shared config logic. | @@ -59,7 +58,7 @@ real-world PHP projects. | Trait | Description | |---------------|------------------------------------------------------------------------------------------------| -| **HookTrait** | Generic hook system for on-get/on-set callbacks. Used by `DynamicConfig` & `HookedCollection`. | +| **HookTrait** | Generic hook system for on-get/on-set callbacks. Used by `Config`, `LazyFileConfig`, and `HookedCollection`. | | **DTOTrait** | Utility trait for DTO-like behavior: populate, extract, cast arrays/objects easily. | @@ -140,12 +139,12 @@ $flat = DotNotation::flatten($user); // [ 'profile.name' => 'Alice', 'profile.email' => 'alice@example.com' ] ``` -### 🔹 Dynamic Config with Hooks +### 🔹 Config Hooks (Explicit) ```php -use Infocyph\ArrayKit\Config\DynamicConfig; +use Infocyph\ArrayKit\Config\Config; -$config = new DynamicConfig(); +$config = new Config(); // Load from file $config->loadFile(__DIR__.'/config.php'); @@ -157,8 +156,8 @@ $config->onSet('auth.password', fn($v) => password_hash($v, PASSWORD_BCRYPT)); $config->onGet('secure.key', fn($v) => decrypt($v)); // Use it -$config->set('auth.password', 'secret123'); -$hashed = $config->get('auth.password'); +$config->setWithHooks('auth.password', 'secret123'); +$hashed = $config->getWithHooks('auth.password'); ``` ### 🔹 Hooked Collection diff --git a/docs/config.rst b/docs/config.rst index 385f3f7..b5d2948 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -7,9 +7,9 @@ Classes: - ``Infocyph\ArrayKit\Config\Config`` - ``Infocyph\ArrayKit\Config\LazyFileConfig`` -- ``Infocyph\ArrayKit\Config\DynamicConfig`` -``DynamicConfig`` extends ``Config`` by adding value hooks. +``Config`` supports optional hooks via explicit ``getWithHooks()``, +``setWithHooks()``, and ``fillWithHooks()`` methods. ``LazyFileConfig`` loads namespace files only on first keyed access. Loading Configuration @@ -129,23 +129,24 @@ Array-Value Helpers // ['cors', 'auth', 'throttle'] $middleware = $config->get('middleware'); -DynamicConfig Hooks -------------------- +Config Hooks (Explicit) +----------------------- -``DynamicConfig`` allows per-key transformation on read/write. +``Config`` allows per-key transformation on read/write, while keeping +``get()/set()/fill()`` hook-free for maximum base-path performance. .. code-block:: php onSet('user.name', fn ($v) => strtoupper((string) $v)); $config->onGet('user.name', fn ($v) => strtolower((string) $v)); - $config->set('user.name', 'Alice'); - echo $config->get('user.name'); // alice + $config->setWithHooks('user.name', 'Alice'); + echo $config->getWithHooks('user.name'); // alice Bulk operations with hooks: @@ -154,12 +155,12 @@ Bulk operations with hooks: onSet('user.email', fn ($v) => trim((string) $v)); - $config->set([ + $config->setWithHooks([ 'user.name' => 'JOHN', 'user.email' => ' john@example.com ', ]); - $vals = $config->get(['user.name', 'user.email']); + $vals = $config->getWithHooks(['user.name', 'user.email']); Practical Pattern ----------------- @@ -169,15 +170,15 @@ Use config as a mutable runtime container for app setup: .. code-block:: php loadFile(__DIR__.'/config.php'); // Normalize selected runtime values $config->onSet('app.timezone', fn ($v) => trim((string) $v)); $config->onGet('app.timezone', fn ($v) => strtoupper((string) $v)); - $config->set('app.timezone', ' utc '); - $tz = $config->get('app.timezone'); // UTC + $config->setWithHooks('app.timezone', ' utc '); + $tz = $config->getWithHooks('app.timezone'); // UTC LazyFileConfig -------------- @@ -231,9 +232,9 @@ LazyFileConfig methods: - ``preload()``, ``isLoaded()``, ``loadedNamespaces()`` - ``all()`` (throws by design) -DynamicConfig methods: +Hook-aware methods (Config and LazyFileConfig): -- ``get()`` (hook-aware override) -- ``set()`` (hook-aware override) -- ``fill()`` (hook-aware override) +- ``getWithHooks()`` +- ``setWithHooks()`` +- ``fillWithHooks()`` - ``onGet()``, ``onSet()`` diff --git a/docs/quick-usage.rst b/docs/quick-usage.rst index 833b81c..6baeeac 100644 --- a/docs/quick-usage.rst +++ b/docs/quick-usage.rst @@ -60,12 +60,12 @@ Config + Hooks Example .. code-block:: php set('auth.password', 'secret'); $config->onGet('auth.password', fn ($v) => strtoupper((string) $v)); - echo $config->get('auth.password'); // SECRET + echo $config->getWithHooks('auth.password'); // SECRET Global Helper Example --------------------- diff --git a/docs/rule-reference.rst b/docs/rule-reference.rst index 03050f0..139ee18 100644 --- a/docs/rule-reference.rst +++ b/docs/rule-reference.rst @@ -58,10 +58,10 @@ Runtime configuration with hooks: .. code-block:: php onSet('app.name', fn ($v) => trim((string) $v)); - $config->set('app.name', ' ArrayKit '); - echo $config->get('app.name'); // ArrayKit + $config->setWithHooks('app.name', ' ArrayKit '); + echo $config->getWithHooks('app.name'); // ArrayKit Static array utilities for data shaping: @@ -376,16 +376,16 @@ LazyFileConfig loads top-level config files on first keyed access: public function loadedNamespaces(): array public function all(): array // throws (design choice) -DynamicConfig +Config Hook-Aware Variants -------------------------------------- -DynamicConfig extends Config behavior with hooks and overrides: +Both Config and LazyFileConfig expose explicit hook-aware methods: .. code-block:: php - public function get(int|string|array|null $key = null, mixed $default = null): mixed - public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool - public function fill(string|array $key, mixed $value = null): bool + public function getWithHooks(int|string|array|null $key = null, mixed $default = null): mixed + public function setWithHooks(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool + public function fillWithHooks(string|array $key, mixed $value = null): bool public function onGet(string $offset, callable $callback): static public function onSet(string $offset, callable $callback): static diff --git a/docs/traits-and-helpers.rst b/docs/traits-and-helpers.rst index f99f14e..968c144 100644 --- a/docs/traits-and-helpers.rst +++ b/docs/traits-and-helpers.rst @@ -81,7 +81,8 @@ Main methods: ``HookTrait`` is used internally by: - ``Infocyph\ArrayKit\Collection\HookedCollection`` -- ``Infocyph\ArrayKit\Config\DynamicConfig`` +- ``Infocyph\ArrayKit\Config\Config`` +- ``Infocyph\ArrayKit\Config\LazyFileConfig`` HookedCollection Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -103,20 +104,20 @@ HookedCollection Integration $c['role'] = 'admin'; echo $c['role']; // Role: admin -DynamicConfig Integration -~~~~~~~~~~~~~~~~~~~~~~~~~ +Config Integration +~~~~~~~~~~~~~~~~~~ .. code-block:: php onSet('user.email', fn ($v) => trim((string) $v)); $config->onGet('user.email', fn ($v) => strtolower((string) $v)); - $config->set('user.email', ' ALICE@EXAMPLE.COM '); - echo $config->get('user.email'); // alice@example.com + $config->setWithHooks('user.email', ' ALICE@EXAMPLE.COM '); + echo $config->getWithHooks('user.email'); // alice@example.com Multiple Hooks on Same Key ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -129,7 +130,7 @@ Hooks run in registration order: $config->onSet('username', fn ($v) => trim((string) $v)); $config->onSet('username', fn ($v) => strtolower((string) $v)); - $config->set('username', ' ALICE '); // becomes "alice" + $config->setWithHooks('username', ' ALICE '); // becomes "alice" Global Helper Functions ----------------------- diff --git a/src/Config/Config.php b/src/Config/Config.php index 07ebed1..c4a894f 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -4,14 +4,75 @@ namespace Infocyph\ArrayKit\Config; +use Infocyph\ArrayKit\traits\HookTrait; + /** * Class Config * - * Example usage of the BaseConfigTrait to provide - * core configuration handling, plus any additional - * features from the Multi trait. + * Provides base configuration storage with optional hook-aware variants. + * + * Core methods from BaseConfigTrait (`get`, `set`, `fill`) remain fast and + * hook-free. Hook processing is explicit through `getWithHooks`, + * `setWithHooks`, and `fillWithHooks`. */ class Config { use BaseConfigTrait; + use HookTrait; + + /** + * Hook-aware variant of fill(). + */ + public function fillWithHooks(string|array $key, mixed $value = null): bool + { + if (is_array($key)) { + $processed = []; + foreach ($key as $path => $entry) { + $processed[$path] = $this->processValue($path, $entry, 'set'); + } + + return $this->fill($processed); + } + + $processed = $this->processValue($key, $value, 'set'); + + return $this->fill($key, $processed); + } + + /** + * Hook-aware variant of get(). + */ + public function getWithHooks(int|string|array|null $key = null, mixed $default = null): mixed + { + $value = $this->get($key, $default); + + if (is_array($key)) { + foreach ($value as $path => $entry) { + $value[$path] = $this->processValue($path, $entry, 'get'); + } + + return $value; + } + + return $this->processValue($key, $value, 'get'); + } + + /** + * Hook-aware variant of set(). + */ + public function setWithHooks(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool + { + if (is_array($key)) { + $processed = []; + foreach ($key as $path => $entry) { + $processed[$path] = $this->processValue($path, $entry, 'set'); + } + + return $this->set($processed, null, $overwrite); + } + + $processedValue = $this->processValue($key, $value, 'set'); + + return $this->set($key, $processedValue, $overwrite); + } } diff --git a/src/Config/DynamicConfig.php b/src/Config/DynamicConfig.php deleted file mode 100644 index c6774b8..0000000 --- a/src/Config/DynamicConfig.php +++ /dev/null @@ -1,89 +0,0 @@ - value] - * @param mixed|null $value The value to set if missing - * @return bool True on success - */ - public function fill(string|array $key, mixed $value = null): bool - { - if (is_array($key)) { - $processed = []; - foreach ($key as $path => $entry) { - $processed[$path] = $this->processValue($path, $entry, 'set'); - } - - DotNotation::fill($this->items, $processed); - - return true; - } - - $processed = $this->processValue($key, $value, 'set'); - DotNotation::fill($this->items, $key, $processed); - return true; - } - - - /** - * Retrieves a configuration value by dot-notation key, applying any "on get" hooks. - * - * @param int|string|array|null $key The key(s) to retrieve (supports dot notation) - * @param mixed $default The default value to return if the key is not found - * @return mixed The retrieved value - */ - public function get(int|string|array|null $key = null, mixed $default = null): mixed - { - $value = DotNotation::get($this->items, $key, $default); - - if (is_array($key)) { - foreach ($value as $path => $entry) { - $value[$path] = $this->processValue($path, $entry, 'get'); - } - - return $value; - } - - return $this->processValue($key, $value, 'get'); - } - - - /** - * Sets a configuration value by dot-notation key, applying any "on set" hooks. - * - * @param string|array|null $key The key to set (supports dot notation) - * @param mixed $value The value to set - * @param bool $overwrite If true, overwrite existing values; otherwise, fill in missing (default true) - * @return bool True on success - */ - public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool - { - if (is_array($key)) { - $processed = []; - foreach ($key as $path => $entry) { - $processed[$path] = $this->processValue($path, $entry, 'set'); - } - - return DotNotation::set($this->items, $processed, null, $overwrite); - } - - $processedValue = $this->processValue($key, $value, 'set'); - - return DotNotation::set($this->items, $key, $processedValue, $overwrite); - } -} diff --git a/src/traits/HookTrait.php b/src/traits/HookTrait.php index 188ea16..d5a4246 100644 --- a/src/traits/HookTrait.php +++ b/src/traits/HookTrait.php @@ -11,8 +11,8 @@ * Each hook is a callable that transforms the value at runtime. * * Example usage: - * $this->onGet('username', fn($value) => strtolower($value)); - * $this->onSet('password', fn($plain) => password_hash($plain, PASSWORD_BCRYPT)); + * $this->onGet('username', fn($value) => strtolower((string) $value)); + * $this->onSet('password', fn($plain) => password_hash((string) $plain, PASSWORD_BCRYPT)); */ trait HookTrait { diff --git a/tests/Feature/ConfigHooksTest.php b/tests/Feature/ConfigHooksTest.php new file mode 100644 index 0000000..6149f55 --- /dev/null +++ b/tests/Feature/ConfigHooksTest.php @@ -0,0 +1,56 @@ +config = new Config(); +}); + +it('applies get hooks only when getWithHooks is used', function () { + $this->config->set('site.title', 'ArRayKit'); + $this->config->onGet('site.title', fn($value) => strtolower((string) $value)); + + expect($this->config->get('site.title'))->toBe('ArRayKit') + ->and($this->config->getWithHooks('site.title'))->toBe('arraykit'); +}); + +it('applies set hooks only when setWithHooks is used', function () { + $this->config->onSet('user.name', fn($value) => strtoupper((string) $value)); + + $this->config->set('user.name', 'john'); + expect($this->config->get('user.name'))->toBe('john'); + + $this->config->setWithHooks('user.name', 'alice'); + expect($this->config->get('user.name'))->toBe('ALICE'); +}); + +it('supports hook-aware bulk set and bulk get operations', function () { + $this->config->onSet('user.name', fn($value) => strtoupper((string) $value)); + $this->config->onGet('user.name', fn($value) => strtolower((string) $value)); + + $this->config->setWithHooks([ + 'user.name' => 'ALICE', + 'user.email' => 'alice@example.com', + ]); + + expect($this->config->getWithHooks(['user.name', 'user.email']))->toBe([ + 'user.name' => 'alice', + 'user.email' => 'alice@example.com', + ]); +}); + +it('supports hook-aware fill without overwriting existing keys', function () { + $this->config->set('app.name', 'ArrayKit'); + $this->config->onSet('app.name', fn($value) => strtoupper((string) $value)); + $this->config->onSet('app.env', fn($value) => strtoupper((string) $value)); + + $this->config->fillWithHooks([ + 'app.name' => 'should-not-replace', + 'app.env' => 'local', + ]); + + expect($this->config->get('app.name'))->toBe('ArrayKit') + ->and($this->config->get('app.env'))->toBe('LOCAL'); +}); diff --git a/tests/Feature/DynamicConfigTest.php b/tests/Feature/DynamicConfigTest.php deleted file mode 100644 index bb748e8..0000000 --- a/tests/Feature/DynamicConfigTest.php +++ /dev/null @@ -1,58 +0,0 @@ -dynamic = new DynamicConfig(); -}); - -it('allows hooking on get operations', function () { - // Suppose we set a value - $this->dynamic->set('site.title', 'ArRayKit'); - - // Then define a hook that lowercases on get - $this->dynamic->onGet('site.title', fn($val) => strtolower($val)); - - // Now retrieving it should be lowercased - expect($this->dynamic->get('site.title'))->toBe('arraykit'); -}); - -it('allows hooking on set operations', function () { - // Hook that uppercases the value before storing - $this->dynamic->onSet('user.name', fn($val) => strtoupper($val)); - - $this->dynamic->set('user.name', 'john'); - // The stored value should be uppercase - expect($this->dynamic->get('user.name'))->toBe('JOHN'); -}); - -it('supports hooks with bulk set/get operations', function () { - $this->dynamic->onSet('user.name', fn ($value) => strtoupper($value)); - $this->dynamic->onGet('user.name', fn ($value) => strtolower($value)); - - $this->dynamic->set([ - 'user.name' => 'alice', - 'user.email' => 'alice@example.com', - ]); - - expect($this->dynamic->get(['user.name', 'user.email']))->toBe([ - 'user.name' => 'alice', - 'user.email' => 'alice@example.com', - ]); -}); - -it('supports bulk fill operations without overwriting existing keys', function () { - $this->dynamic->set('app.name', 'ArrayKit'); - - $this->dynamic->fill([ - 'app.name' => 'ShouldNotReplace', - 'app.env' => 'local', - ]); - - expect($this->dynamic->get('app.name')) - ->toBe('ArrayKit') - ->and($this->dynamic->get('app.env'))->toBe('local'); -}); diff --git a/tests/Feature/LazyFileConfigTest.php b/tests/Feature/LazyFileConfigTest.php index 8cdc2fd..c7e9b76 100644 --- a/tests/Feature/LazyFileConfigTest.php +++ b/tests/Feature/LazyFileConfigTest.php @@ -178,3 +178,34 @@ function lazyConfigItems(LazyFileConfig $config): array expect(fn () => $config->preload('invalid.namespace'))->toThrow(InvalidArgumentException::class); }); + +it('supports hook-aware lazy get and set variants', function () { + lazyConfigWriteArrayFile($this->configPath, 'db', ['host' => 'localhost']); + + $config = new LazyFileConfig($this->configPath); + $config->onSet('db.host', fn($value) => strtoupper((string) $value)); + $config->onGet('db.host', fn($value) => strtolower((string) $value)); + + $config->setWithHooks('db.host', 'INTERNAL'); + + expect($config->get('db.host'))->toBe('INTERNAL') + ->and($config->getWithHooks('db.host'))->toBe('internal'); +}); + +it('supports hook-aware lazy bulk operations', function () { + lazyConfigWriteArrayFile($this->configPath, 'db', ['host' => 'localhost', 'port' => 3306]); + + $config = new LazyFileConfig($this->configPath); + $config->onSet('db.host', fn($value) => strtoupper((string) $value)); + $config->onGet('db.host', fn($value) => strtolower((string) $value)); + + $config->setWithHooks([ + 'db.host' => 'INTERNAL', + 'db.port' => 5432, + ]); + + expect($config->getWithHooks(['db.host', 'db.port']))->toBe([ + 'db.host' => 'internal', + 'db.port' => 5432, + ]); +});