From f6a95308e9b26909e973dbd5ee4c8b844c26778b Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 18 Feb 2026 10:04:13 +0530 Subject: [PATCH 01/19] Added Context class --- app/Config/Logger.php | 18 ++++ system/Config/Services.php | 15 +++ system/Context/Context.php | 189 +++++++++++++++++++++++++++++++++++++ system/Log/Logger.php | 15 +++ 4 files changed, 237 insertions(+) create mode 100644 system/Context/Context.php diff --git a/app/Config/Logger.php b/app/Config/Logger.php index 799dc2c39080..d971ecc43fae 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -51,6 +51,24 @@ class Logger extends BaseConfig */ public string $dateFormat = 'Y-m-d H:i:s'; + /** + * -------------------------------------------------------------------------- + * Whether to log the global context + * -------------------------------------------------------------------------- + * + * You can enable/disable logging of global context data, which comes from the + * `CodeIgniter\Context\Context` class. This data is automatically included in + * logs, and can be set using the `set()` method of the Context class. This is + * useful for including additional information in your logs, such as user IDs, + * request IDs, etc. + * + * **NOTE:** This **DOES NOT** include any data that has been marked as hidden + * using the `setHidden()` method of the Context class. + * + * @var bool + */ + public bool $logGlobalContext = false; + /** * -------------------------------------------------------------------------- * Log Handlers diff --git a/system/Config/Services.php b/system/Config/Services.php index 3878911c8cbf..07f5d9bd2398 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -18,6 +18,7 @@ use CodeIgniter\Cache\ResponseCache; use CodeIgniter\CLI\Commands; use CodeIgniter\CodeIgniter; +use CodeIgniter\Context\Context; use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Database\MigrationRunner; use CodeIgniter\Debug\Exceptions; @@ -872,4 +873,18 @@ public static function typography(bool $getShared = true) return new Typography(); } + + /** + * The Context class provides a way to store and retrieve static data throughout requests. + * + * @return Context + */ + public static function context(bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('context'); + } + + return new Context(); + } } diff --git a/system/Context/Context.php b/system/Context/Context.php new file mode 100644 index 000000000000..208880f98e5c --- /dev/null +++ b/system/Context/Context.php @@ -0,0 +1,189 @@ + + */ + protected array $data; + + /** + * The data that is stored, but not included in logs. + * + * @var array + */ + private array $hiddenData; + + /** + * Constructor + */ + public function __construct() + { + $this->data = []; + $this->hiddenData = []; + } + + /** + * Set a key-value pair to the context. + * + * @param string|array $key The key to identify the data. Can be a string or an array of key-value pairs. + * @param mixed $value The value to be stored in the context. + * @return $this + */ + public function set(string|array $key, mixed $value): self + { + if (is_array($key)) { + $this->data = array_merge($this->data, $key); + return $this; + } + + $this->data[$key] = $value; + return $this; + } + + /** + * Set a hidden key-value pair to the context. This data will not be included in logs. + * + * @param string|array $key The key to identify the data. Can be a string or an array of key-value pairs. + * @param mixed $value The value to be stored in the context. + * @return $this + */ + public function setHidden(string|array $key, mixed $value): self + { + if (is_array($key)) { + $this->hiddenData = array_merge($this->hiddenData, $key); + return $this; + } + + $this->hiddenData[$key] = $value; + return $this; + } + + /** + * Get a value from the context by its key, or return a default value if the key does not exist. + * + * @param string $key The key to identify the data. + * @param mixed|null $default The default value to return if the key does not exist in the context. + * @return mixed The value associated with the key, or the default value if the key does not exist. + */ + public function get(string $key, mixed $default = null): mixed + { + return $this->data[$key] ?? $default; + } + + /** + * Get all data from the context + * + * @return array An array of all key-value pairs in the context. + */ + public function getAll(): array + { + return $this->data; + } + + /** + * Get a hidden value from the context by its key, or return a default value if the key does not exist. + * + * @param string $key The key to identify the data. + * @param mixed|null $default The default value to return if the key does not exist in the context. + * @return mixed The value associated with the key, or the default value if the key does not exist. + */ + public function getHidden(string $key, mixed $default = null): mixed + { + return $this->hiddenData[$key] ?? $default; + } + + /** + * Get all hidden data from the context + * + * @return array An array of all key-value pairs in the hidden context. + */ + public function getAllHidden(): array + { + return $this->hiddenData; + } + + /** + * Check if a key exists in the context. + * + * @param string $key The key to check for existence in the context. + * @return bool True if the key exists in the context, false otherwise. + */ + public function has(string $key): bool + { + return array_key_exists($key, $this->data); + } + + /** + * Check if a key exists in the hidden context. + * + * @param string $key The key to check for existence in the hidden context. + * @return bool True if the key exists in the hidden context, false otherwise. + */ + public function hasHidden(string $key): bool + { + return array_key_exists($key, $this->hiddenData); + } + + /** + * Remove a key-value pair from the context by its key. + * + * @param string $key The key to identify the data to be removed from the context. + * @return $this + */ + public function remove(string $key): self + { + unset($this->data[$key]); + return $this; + } + + /** + * Remove a key-value pair from the hidden context by its key. + * + * @param string $key The key to identify the data to be removed from the hidden context. + * @return $this + */ + public function removeHidden(string $key): self + { + unset($this->hiddenData[$key]); + return $this; + } + + /** + * Clear all data from the context, including hidden data. + * + * @return $this + */ + public function clearAll(): self + { + $this->clear(); + $this->clearHidden(); + return $this; + } + + /** + * Clear all data from the context. + * + * @return $this + */ + public function clear(): self + { + $this->data = []; + return $this; + } + + /** + * Clear all hidden data from the context. + * + * @return $this + */ + public function clearHidden(): self + { + $this->hiddenData = []; + return $this; + } +} diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 8308ddaf94f7..c1e868a944dd 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -114,6 +114,15 @@ class Logger implements LoggerInterface */ protected $cacheLogs = false; + /** + * Whether to log the global context data. + * + * Set in app/Config/Logger.php + * + * @var bool + */ + protected $logGlobalContext; + /** * Constructor. * @@ -154,6 +163,8 @@ public function __construct($config, bool $debug = CI_DEBUG) if ($this->cacheLogs) { $this->logCache = []; } + + $this->logGlobalContext = $config->logGlobalContext; } /** @@ -252,6 +263,10 @@ public function log($level, string|Stringable $message, array $context = []): vo $message = $this->interpolate($message, $context); + if ($this->logGlobalContext) { + $message .= ' ' . json_encode(service('context')->getAll()); + } + if ($this->cacheLogs) { $this->logCache[] = ['level' => $level, 'msg' => $message]; } From 30cdc191acff06a6cc24679add35de5cc9ca649d Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 18 Feb 2026 11:59:00 +0530 Subject: [PATCH 02/19] Added some convenient methods --- system/Context/Context.php | 104 ++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/system/Context/Context.php b/system/Context/Context.php index 208880f98e5c..b9b77ab7846f 100644 --- a/system/Context/Context.php +++ b/system/Context/Context.php @@ -34,7 +34,7 @@ public function __construct() * @param mixed $value The value to be stored in the context. * @return $this */ - public function set(string|array $key, mixed $value): self + public function set(string|array $key, mixed $value = null): self { if (is_array($key)) { $this->data = array_merge($this->data, $key); @@ -52,7 +52,7 @@ public function set(string|array $key, mixed $value): self * @param mixed $value The value to be stored in the context. * @return $this */ - public function setHidden(string|array $key, mixed $value): self + public function setHidden(string|array $key, mixed $value = null): self { if (is_array($key)) { $this->hiddenData = array_merge($this->hiddenData, $key); @@ -75,6 +75,34 @@ public function get(string $key, mixed $default = null): mixed return $this->data[$key] ?? $default; } + /** + * Get only the specified keys from the context. If a key does not exist, it will be ignored. + * + * @param string|array $keys An array of keys to retrieve from the context. + * @return array An array of key-value pairs for the specified keys that exist in the context. + */ + public function getOnly(string|array $keys): array + { + if (is_string($keys)) { + $keys = [$keys]; + } + return array_filter($this->data, fn($k) => in_array($k, $keys), ARRAY_FILTER_USE_KEY); + } + + /** + * Get all keys from the context except the specified keys. + * + * @param string|array $keys An array of keys to exclude from the context. + * @return array An array of key-value pairs for all keys in the context except the specified keys. + */ + public function getExcept(string|array $keys): array + { + if (is_string($keys)) { + $keys = [$keys]; + } + return array_filter($this->data, fn($k) => !in_array($k, $keys), ARRAY_FILTER_USE_KEY); + } + /** * Get all data from the context * @@ -97,6 +125,34 @@ public function getHidden(string $key, mixed $default = null): mixed return $this->hiddenData[$key] ?? $default; } + /** + * Get only the specified keys from the hidden context. If a key does not exist, it will be ignored. + * + * @param string|array $keys An array of keys to retrieve from the hidden context. + * @return array An array of key-value pairs for the specified keys that exist in the hidden context. + */ + public function getOnlyHidden(string|array $keys): array + { + if (is_string($keys)) { + $keys = [$keys]; + } + return array_filter($this->hiddenData, fn($k) => in_array($k, $keys), ARRAY_FILTER_USE_KEY); + } + + /** + * Get all keys from the hidden context except the specified keys. + * + * @param string|array $keys An array of keys to exclude from the hidden context. + * @return array An array of key-value pairs for all keys in the hidden context except the specified keys. + */ + public function getExceptHidden(string|array $keys): array + { + if (is_string($keys)) { + $keys = [$keys]; + } + return array_filter($this->hiddenData, fn($k) => !in_array($k, $keys), ARRAY_FILTER_USE_KEY); + } + /** * Get all hidden data from the context * @@ -107,6 +163,17 @@ public function getAllHidden(): array return $this->hiddenData; } + /** + * Check if a key does not exist in the context. Exactly the opposite of `has()`. + * + * @param string $key The key to check for non-existence in the context. + * @return bool True if the key does not exist in the context, false otherwise. + */ + public function missing(string $key): bool + { + return !$this->has($key); + } + /** * Check if a key exists in the context. * @@ -118,6 +185,17 @@ public function has(string $key): bool return array_key_exists($key, $this->data); } + /** + * Check if a key does not exist in the hidden context. Exactly the opposite of `hasHidden()`. + * + * @param string $key The key to check for non-existence in the hidden context. + * @return bool True if the key does not exist in the hidden context, false otherwise. + */ + public function missingHidden(string $key): bool + { + return !$this->hasHidden($key); + } + /** * Check if a key exists in the hidden context. * @@ -132,11 +210,18 @@ public function hasHidden(string $key): bool /** * Remove a key-value pair from the context by its key. * - * @param string $key The key to identify the data to be removed from the context. + * @param string|array $key The key to identify the data to be removed from the context. * @return $this */ - public function remove(string $key): self + public function remove(string|array $key): self { + if (is_array($key)) { + foreach ($key as $k) { + unset($this->data[$k]); + } + return $this; + } + unset($this->data[$key]); return $this; } @@ -144,11 +229,18 @@ public function remove(string $key): self /** * Remove a key-value pair from the hidden context by its key. * - * @param string $key The key to identify the data to be removed from the hidden context. + * @param string|array $key The key to identify the data to be removed from the hidden context. * @return $this */ - public function removeHidden(string $key): self + public function removeHidden(string|array $key): self { + if (is_array($key)) { + foreach ($key as $k) { + unset($this->hiddenData[$k]); + } + return $this; + } + unset($this->hiddenData[$key]); return $this; } From a1666782c9374e9ba6b7eb60154cd3daaf984b0b Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 18 Feb 2026 11:59:30 +0530 Subject: [PATCH 03/19] Enhance logger to properly include global context --- system/Log/Logger.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/Log/Logger.php b/system/Log/Logger.php index c1e868a944dd..22eb3de48637 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -264,7 +264,10 @@ public function log($level, string|Stringable $message, array $context = []): vo $message = $this->interpolate($message, $context); if ($this->logGlobalContext) { - $message .= ' ' . json_encode(service('context')->getAll()); + $globalContext = service('context')->getAll(); + if (is_array($globalContext) && $globalContext !== []) { + $message .= ' ' . json_encode($globalContext); + } } if ($this->cacheLogs) { From 43c2d5202ecca8768db9e79d01f42a250cbc69be Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 18 Feb 2026 11:59:51 +0530 Subject: [PATCH 04/19] Add unit tests for Context class functionality --- tests/system/Context/ContextTest.php | 409 +++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 tests/system/Context/ContextTest.php diff --git a/tests/system/Context/ContextTest.php b/tests/system/Context/ContextTest.php new file mode 100644 index 000000000000..d3c47b36cda0 --- /dev/null +++ b/tests/system/Context/ContextTest.php @@ -0,0 +1,409 @@ +assertSame([], $context->getAll()); + $this->assertSame([], $context->getAllHidden()); + } + + public function testSetAndGetSingleValue(): void + { + $context = service('context'); + $context->set('user_id', 123); + + $this->assertSame(123, $context->get('user_id')); + $this->assertNull($context->getHidden('user_id')); // Normal value should not be retrievable with getHidden() + } + + public function testSetAndGetMultipleValues(): void + { + $context = service('context'); + $context->set([ + 'user_id' => 123, + 'username' => 'john_doe', + ]); + + $this->assertSame(123, $context->get('user_id')); + $this->assertSame('john_doe', $context->get('username')); + $this->assertNull($context->getHidden('user_id')); + $this->assertNull($context->getHidden('username')); + } + + public function testSetAndGetSingleHiddenValue(): void + { + $context = service('context'); + $context->setHidden('api_key', 'secret'); + + $this->assertSame('secret', $context->getHidden('api_key')); + $this->assertNull($context->get('api_key')); // Hidden value should not be retrievable with get() + } + + public function testSetAndGetMultipleHiddenValues(): void + { + $context = service('context'); + $context->setHidden([ + 'api_key' => 'secret', + 'token' => 'abc123', + ]); + + $this->assertSame('secret', $context->getHidden('api_key')); + $this->assertSame('abc123', $context->getHidden('token')); + $this->assertNull($context->get('api_key')); + $this->assertNull($context->get('token')); + } + + public function testClear(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->set('username', 'john_doe'); + + $context->clear(); + + $this->assertNull($context->get('user_id')); + $this->assertNull($context->get('username')); + } + + public function testClearDoesntAffectHidden(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('api_key', 'secret123'); + + $context->clear(); + + $this->assertNull($context->get('user_id')); + $this->assertSame('secret123', $context->getHidden('api_key')); // Hidden value should still be retrievable after clear() + } + + public function testClearHidden(): void + { + $context = service('context'); + $context->setHidden('api_key', 'abcdef'); + $context->setHidden('token', 'abc123'); + + $context->clearHidden(); + + $this->assertNull($context->getHidden('api_key')); + $this->assertNull($context->getHidden('token')); + } + + public function testClearHiddenDoesntAffectNormalValues(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('api_key', 'secret123'); + + $context->clearHidden(); + + $this->assertSame(123, $context->get('user_id')); // Normal value should still be retrievable after clearHidden() + $this->assertNull($context->getHidden('api_key')); // Hidden value should be cleared + } + + public function testClearAll(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('api_key', 'secret'); + + $context->clearAll(); + + $this->assertNull($context->get('user_id')); + $this->assertNull($context->getHidden('api_key')); + } + + public function testGetWithDefaultValue(): void + { + $context = service('context'); + + $context->set('user_id', 123); + + $this->assertSame(123, $context->get('user_id', 'default')); // Existing key should return its value, not the default + $this->assertSame('default', $context->get('non_existent_key', 'default')); + } + + public function testGetOnlySingleKey(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->set('username', 'john_doe'); + $context->setHidden('api_key', 'secret'); + + $this->assertSame(['user_id' => 123], $context->getOnly('user_id')); + $this->assertSame(['username' => 'john_doe'], $context->getOnly('username')); + $this->assertSame([], $context->getOnly('non_existent_key')); + } + + public function testGetOnlyMultipleKeys(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->set('username', 'john_doe'); + $context->setHidden('api_key', 'secret'); + + $expected = [ + 'user_id' => 123, + 'username' => 'john_doe', + ]; + $this->assertSame($expected, $context->getOnly(['user_id', 'username', 'non_existent_key'])); // non_existent_key should be ignored + } + + public function testGetExceptSingleKey(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->set('username', 'john_doe'); + $context->setHidden('api_key', 'secret'); + + $expected = [ + 'username' => 'john_doe', + ]; + $this->assertSame($expected, $context->getExcept('user_id')); // user_id should be excluded + } + + public function testGetExceptMultipleKeys(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->set('username', 'john_doe'); + $context->setHidden('api_key', 'secret'); + + $expected = [ + 'username' => 'john_doe', + ]; + $this->assertSame($expected, $context->getExcept(['user_id', 'non_existent_key'])); // user_id should be excluded, non_existent_key should be ignored + } + + public function testGetAll(): void + { + $context = service('context'); + $context->set([ + 'user_id' => 123, + 'username' => 'john_doe', + ]); + + $expected = [ + 'user_id' => 123, + 'username' => 'john_doe', + ]; + + $this->assertSame($expected, $context->getAll()); + } + + public function testGetHiddenWithDefaultValue(): void + { + $context = service('context'); + + $context->setHidden('some_secret_token', '123456abcdefghij'); + + $this->assertSame('123456abcdefghij', $context->getHidden('some_secret_token', 'foo')); // Existing key should return its value, not the default + $this->assertSame('foo', $context->getHidden('api_key', 'foo')); + } + + public function testGetOnlyHiddenSingleKey(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('api_key', 'some_secret_api_key_here'); + + $this->assertSame(['api_key' => 'some_secret_api_key_here'], $context->getOnlyHidden('api_key')); + $this->assertSame([], $context->getOnlyHidden('some_token')); + } + + public function testGetOnlyHiddenMultipleKeys(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('api_key', 'secret'); + $context->setHidden('token', 'abc123'); + + $expected = [ + 'api_key' => 'secret', + 'token' => 'abc123', + ]; + $this->assertSame($expected, $context->getOnlyHidden(['api_key', 'token', 'non_existent_key'])); // non_existent_key should be ignored + } + + public function testGetExceptHiddenSingleKey(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('some_sensitive_user_info', 'abcdefghij'); + $context->setHidden('api_key', 'some_secret_api_key_here'); + + $expected = [ + 'some_sensitive_user_info' => 'abcdefghij', + ]; + + $this->assertSame($expected, $context->getExceptHidden('api_key')); + } + + public function testGetExceptHiddenMultipleKeys(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('token', 'abc123'); + $context->setHidden('api_key', 'secret'); + + $expected = [ + 'token' => 'abc123', + ]; + $this->assertSame($expected, $context->getExceptHidden(['api_key', 'non_existent_key'])); // token should be excluded, non_existent_key should be ignored + } + + public function testGetAllHidden(): void + { + $context = service('context'); + $context->setHidden([ + 'api_key' => 'secret', + 'token' => 'abc123', + ]); + + $expected = [ + 'api_key' => 'secret', + 'token' => 'abc123', + ]; + + $this->assertSame($expected, $context->getAllHidden()); + } + + public function testOverwriteExistingValue(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->set('user_id', 456); // Overwrite existing value + + $this->assertSame(456, $context->get('user_id')); + } + + public function testOverwriteExistingHiddenValue(): void + { + $context = service('context'); + $context->setHidden('api_key', 'secret'); + $context->setHidden('api_key', 'new_secret'); // Overwrite existing hidden value + + $this->assertSame('new_secret', $context->getHidden('api_key')); + } + + public function testSetHiddenDoesNotAffectNormalValues(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->setHidden('user_id', 'hidden_value'); + + $this->assertSame(123, $context->get('user_id')); // Normal value should still be retrievable + $this->assertSame('hidden_value', $context->getHidden('user_id')); // Hidden value should be retrievable with getHidden() + } + + public function testHasKey(): void + { + $context = service('context'); + $this->assertFalse($context->has('user_id')); + + $context->set('user_id', 123); + + $this->assertTrue($context->has('user_id')); + } + + public function testMissingKey(): void + { + $context = service('context'); + $this->assertTrue($context->missing('user_id')); + + $context->set('user_id', 123); + $this->assertFalse($context->missing('user_id')); + } + + public function testHasHiddenKey(): void + { + $context = service('context'); + $this->assertFalse($context->hasHidden('api_key')); + + $context->setHidden('api_key', 'secret'); + $this->assertTrue($context->hasHidden('api_key')); + } + + public function testMissingHiddenKey(): void + { + $context = service('context'); + $this->assertTrue($context->missingHidden('api_key')); + + $context->setHidden('api_key', 'secret'); + $this->assertFalse($context->missingHidden('api_key')); + } + + public function testRemoveSingleValue(): void + { + $context = service('context'); + $context->set('user_id', 123); + $context->set('username', 'john_doe'); + $context->remove('user_id'); + + $this->assertNull($context->get('user_id')); + $this->assertSame('john_doe', $context->get('username')); // Ensure other values are unaffected + } + + public function testRemoveMultipleValues(): void + { + $context = service('context'); + $context->set([ + 'user_id' => 123, + 'username' => 'john_doe', + 'email' => 'john@example.com', + ]); + + $context->remove(['user_id', 'username']); + + $this->assertNull($context->get('user_id')); + $this->assertNull($context->get('username')); + $this->assertSame('john@example.com', $context->get('email')); // Ensure other values are unaffected + } + + public function testRemoveHiddenValue(): void + { + $context = service('context'); + $context->setHidden('api_key', 'secret'); + $context->setHidden('token', 'abc123'); + + $context->removeHidden('api_key'); + $this->assertNull($context->getHidden('api_key')); + $this->assertSame('abc123', $context->getHidden('token')); // Ensure other hidden values are unaffected + } + + public function testRemoveMultipleHiddenValues(): void + { + $context = service('context'); + $context->setHidden([ + 'api_key' => 'secret', + 'token' => 'abc123', + 'session_id' => 'xyz789', + ]); + + $context->removeHidden(['api_key', 'token']); + + $this->assertNull($context->getHidden('api_key')); + $this->assertNull($context->getHidden('token')); + $this->assertSame('xyz789', $context->getHidden('session_id')); // Ensure other hidden values are unaffected + } + + protected function tearDown(): void + { + parent::tearDown(); + + // Clear the context after each test to ensure isolation. + $context = service('context'); + $context->clearAll(); + } +} From b0d5848d230a77df67b4a719e699c5268ae61898 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 18 Feb 2026 12:00:07 +0530 Subject: [PATCH 05/19] Added tests for global context logging behaviour --- tests/system/Log/LoggerTest.php | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index 80d42d6c83b3..ec71b95bcefd 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -36,6 +36,8 @@ protected function tearDown(): void // Reset the current time. Time::setTestNow(); + + service('context')->clearAll(); // Clear any context data that may have been set during tests. } public function testThrowsExceptionWithBadHandlerSettings(): void @@ -438,4 +440,67 @@ public function testDetermineFileNoStackTrace(): void $this->assertSame($expected, $logger->determineFile()); } + + public function testLogsGlobalContext(): void + { + $config = new LoggerConfig(); + $config->logGlobalContext = true; + + $logger = new Logger($config); + + Time::setTestNow('2026-02-18 12:00:00'); + + service('context')->set('foo', 'bar'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message {"foo":"bar"}'; + + $logger->log('debug', 'Test message'); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testDoesNotLogGlobalContext(): void + { + $config = new LoggerConfig(); + $config->logGlobalContext = false; + + $logger = new Logger($config); + + Time::setTestNow('2026-02-18 12:00:00'); + + service('context')->set('foo', 'bar'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message'; + + $logger->log('debug', 'Test message'); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testDoesNotLogHiddenGlobalContext(): void + { + $config = new LoggerConfig(); + $config->logGlobalContext = true; + + $logger = new Logger($config); + + Time::setTestNow('2026-02-18 12:00:00'); + + service('context')->setHidden('secret', 'hidden value'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message'; + + $logger->log('debug', 'Test message'); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } } From f62d577c994ae332713b854183ff72804a31745c Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 18 Feb 2026 12:02:33 +0530 Subject: [PATCH 06/19] cs-fix --- app/Config/Logger.php | 2 - system/Context/Context.php | 92 ++++++++++++++++++++-------- tests/system/Context/ContextTest.php | 33 ++++++---- tests/system/Log/LoggerTest.php | 6 +- 4 files changed, 89 insertions(+), 44 deletions(-) diff --git a/app/Config/Logger.php b/app/Config/Logger.php index d971ecc43fae..e303965a397b 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -64,8 +64,6 @@ class Logger extends BaseConfig * * **NOTE:** This **DOES NOT** include any data that has been marked as hidden * using the `setHidden()` method of the Context class. - * - * @var bool */ public bool $logGlobalContext = false; diff --git a/system/Context/Context.php b/system/Context/Context.php index b9b77ab7846f..1ff836226965 100644 --- a/system/Context/Context.php +++ b/system/Context/Context.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace CodeIgniter\Context; class Context @@ -23,51 +32,58 @@ class Context */ public function __construct() { - $this->data = []; + $this->data = []; $this->hiddenData = []; } /** * Set a key-value pair to the context. * - * @param string|array $key The key to identify the data. Can be a string or an array of key-value pairs. - * @param mixed $value The value to be stored in the context. + * @param array|string $key The key to identify the data. Can be a string or an array of key-value pairs. + * @param mixed $value The value to be stored in the context. + * * @return $this */ - public function set(string|array $key, mixed $value = null): self + public function set(array|string $key, mixed $value = null): self { if (is_array($key)) { $this->data = array_merge($this->data, $key); + return $this; } $this->data[$key] = $value; + return $this; } /** * Set a hidden key-value pair to the context. This data will not be included in logs. * - * @param string|array $key The key to identify the data. Can be a string or an array of key-value pairs. - * @param mixed $value The value to be stored in the context. + * @param array|string $key The key to identify the data. Can be a string or an array of key-value pairs. + * @param mixed $value The value to be stored in the context. + * * @return $this */ - public function setHidden(string|array $key, mixed $value = null): self + public function setHidden(array|string $key, mixed $value = null): self { if (is_array($key)) { $this->hiddenData = array_merge($this->hiddenData, $key); + return $this; } $this->hiddenData[$key] = $value; + return $this; } /** * Get a value from the context by its key, or return a default value if the key does not exist. * - * @param string $key The key to identify the data. + * @param string $key The key to identify the data. * @param mixed|null $default The default value to return if the key does not exist in the context. + * * @return mixed The value associated with the key, or the default value if the key does not exist. */ public function get(string $key, mixed $default = null): mixed @@ -78,29 +94,33 @@ public function get(string $key, mixed $default = null): mixed /** * Get only the specified keys from the context. If a key does not exist, it will be ignored. * - * @param string|array $keys An array of keys to retrieve from the context. + * @param list|string $keys An array of keys to retrieve from the context. + * * @return array An array of key-value pairs for the specified keys that exist in the context. */ - public function getOnly(string|array $keys): array + public function getOnly(array|string $keys): array { if (is_string($keys)) { $keys = [$keys]; } - return array_filter($this->data, fn($k) => in_array($k, $keys), ARRAY_FILTER_USE_KEY); + + return array_filter($this->data, static fn ($k) => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** * Get all keys from the context except the specified keys. * - * @param string|array $keys An array of keys to exclude from the context. + * @param list|string $keys An array of keys to exclude from the context. + * * @return array An array of key-value pairs for all keys in the context except the specified keys. */ - public function getExcept(string|array $keys): array + public function getExcept(array|string $keys): array { if (is_string($keys)) { $keys = [$keys]; } - return array_filter($this->data, fn($k) => !in_array($k, $keys), ARRAY_FILTER_USE_KEY); + + return array_filter($this->data, static fn ($k) => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** @@ -116,8 +136,9 @@ public function getAll(): array /** * Get a hidden value from the context by its key, or return a default value if the key does not exist. * - * @param string $key The key to identify the data. + * @param string $key The key to identify the data. * @param mixed|null $default The default value to return if the key does not exist in the context. + * * @return mixed The value associated with the key, or the default value if the key does not exist. */ public function getHidden(string $key, mixed $default = null): mixed @@ -128,29 +149,33 @@ public function getHidden(string $key, mixed $default = null): mixed /** * Get only the specified keys from the hidden context. If a key does not exist, it will be ignored. * - * @param string|array $keys An array of keys to retrieve from the hidden context. + * @param list|string $keys An array of keys to retrieve from the hidden context. + * * @return array An array of key-value pairs for the specified keys that exist in the hidden context. */ - public function getOnlyHidden(string|array $keys): array + public function getOnlyHidden(array|string $keys): array { if (is_string($keys)) { $keys = [$keys]; } - return array_filter($this->hiddenData, fn($k) => in_array($k, $keys), ARRAY_FILTER_USE_KEY); + + return array_filter($this->hiddenData, static fn ($k) => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** * Get all keys from the hidden context except the specified keys. * - * @param string|array $keys An array of keys to exclude from the hidden context. + * @param list|string $keys An array of keys to exclude from the hidden context. + * * @return array An array of key-value pairs for all keys in the hidden context except the specified keys. */ - public function getExceptHidden(string|array $keys): array + public function getExceptHidden(array|string $keys): array { if (is_string($keys)) { $keys = [$keys]; } - return array_filter($this->hiddenData, fn($k) => !in_array($k, $keys), ARRAY_FILTER_USE_KEY); + + return array_filter($this->hiddenData, static fn ($k) => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** @@ -167,17 +192,19 @@ public function getAllHidden(): array * Check if a key does not exist in the context. Exactly the opposite of `has()`. * * @param string $key The key to check for non-existence in the context. + * * @return bool True if the key does not exist in the context, false otherwise. */ public function missing(string $key): bool { - return !$this->has($key); + return ! $this->has($key); } /** * Check if a key exists in the context. * * @param string $key The key to check for existence in the context. + * * @return bool True if the key exists in the context, false otherwise. */ public function has(string $key): bool @@ -189,17 +216,19 @@ public function has(string $key): bool * Check if a key does not exist in the hidden context. Exactly the opposite of `hasHidden()`. * * @param string $key The key to check for non-existence in the hidden context. + * * @return bool True if the key does not exist in the hidden context, false otherwise. */ public function missingHidden(string $key): bool { - return !$this->hasHidden($key); + return ! $this->hasHidden($key); } /** * Check if a key exists in the hidden context. * * @param string $key The key to check for existence in the hidden context. + * * @return bool True if the key exists in the hidden context, false otherwise. */ public function hasHidden(string $key): bool @@ -210,38 +239,44 @@ public function hasHidden(string $key): bool /** * Remove a key-value pair from the context by its key. * - * @param string|array $key The key to identify the data to be removed from the context. + * @param list|string $key The key to identify the data to be removed from the context. + * * @return $this */ - public function remove(string|array $key): self + public function remove(array|string $key): self { if (is_array($key)) { foreach ($key as $k) { unset($this->data[$k]); } + return $this; } unset($this->data[$key]); + return $this; } /** * Remove a key-value pair from the hidden context by its key. * - * @param string|array $key The key to identify the data to be removed from the hidden context. + * @param list|string $key The key to identify the data to be removed from the hidden context. + * * @return $this */ - public function removeHidden(string|array $key): self + public function removeHidden(array|string $key): self { if (is_array($key)) { foreach ($key as $k) { unset($this->hiddenData[$k]); } + return $this; } unset($this->hiddenData[$key]); + return $this; } @@ -254,6 +289,7 @@ public function clearAll(): self { $this->clear(); $this->clearHidden(); + return $this; } @@ -265,6 +301,7 @@ public function clearAll(): self public function clear(): self { $this->data = []; + return $this; } @@ -276,6 +313,7 @@ public function clear(): self public function clearHidden(): self { $this->hiddenData = []; + return $this; } } diff --git a/tests/system/Context/ContextTest.php b/tests/system/Context/ContextTest.php index d3c47b36cda0..09f50b8561d9 100644 --- a/tests/system/Context/ContextTest.php +++ b/tests/system/Context/ContextTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace CodeIgniter\Context; use CodeIgniter\Test\CIUnitTestCase; @@ -31,7 +40,7 @@ public function testSetAndGetMultipleValues(): void { $context = service('context'); $context->set([ - 'user_id' => 123, + 'user_id' => 123, 'username' => 'john_doe', ]); @@ -55,7 +64,7 @@ public function testSetAndGetMultipleHiddenValues(): void $context = service('context'); $context->setHidden([ 'api_key' => 'secret', - 'token' => 'abc123', + 'token' => 'abc123', ]); $this->assertSame('secret', $context->getHidden('api_key')); @@ -154,7 +163,7 @@ public function testGetOnlyMultipleKeys(): void $context->setHidden('api_key', 'secret'); $expected = [ - 'user_id' => 123, + 'user_id' => 123, 'username' => 'john_doe', ]; $this->assertSame($expected, $context->getOnly(['user_id', 'username', 'non_existent_key'])); // non_existent_key should be ignored @@ -190,12 +199,12 @@ public function testGetAll(): void { $context = service('context'); $context->set([ - 'user_id' => 123, + 'user_id' => 123, 'username' => 'john_doe', ]); $expected = [ - 'user_id' => 123, + 'user_id' => 123, 'username' => 'john_doe', ]; @@ -231,7 +240,7 @@ public function testGetOnlyHiddenMultipleKeys(): void $expected = [ 'api_key' => 'secret', - 'token' => 'abc123', + 'token' => 'abc123', ]; $this->assertSame($expected, $context->getOnlyHidden(['api_key', 'token', 'non_existent_key'])); // non_existent_key should be ignored } @@ -268,12 +277,12 @@ public function testGetAllHidden(): void $context = service('context'); $context->setHidden([ 'api_key' => 'secret', - 'token' => 'abc123', + 'token' => 'abc123', ]); $expected = [ 'api_key' => 'secret', - 'token' => 'abc123', + 'token' => 'abc123', ]; $this->assertSame($expected, $context->getAllHidden()); @@ -359,9 +368,9 @@ public function testRemoveMultipleValues(): void { $context = service('context'); $context->set([ - 'user_id' => 123, + 'user_id' => 123, 'username' => 'john_doe', - 'email' => 'john@example.com', + 'email' => 'john@example.com', ]); $context->remove(['user_id', 'username']); @@ -386,8 +395,8 @@ public function testRemoveMultipleHiddenValues(): void { $context = service('context'); $context->setHidden([ - 'api_key' => 'secret', - 'token' => 'abc123', + 'api_key' => 'secret', + 'token' => 'abc123', 'session_id' => 'xyz789', ]); diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index ec71b95bcefd..136dc4332bd5 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -443,7 +443,7 @@ public function testDetermineFileNoStackTrace(): void public function testLogsGlobalContext(): void { - $config = new LoggerConfig(); + $config = new LoggerConfig(); $config->logGlobalContext = true; $logger = new Logger($config); @@ -464,7 +464,7 @@ public function testLogsGlobalContext(): void public function testDoesNotLogGlobalContext(): void { - $config = new LoggerConfig(); + $config = new LoggerConfig(); $config->logGlobalContext = false; $logger = new Logger($config); @@ -485,7 +485,7 @@ public function testDoesNotLogGlobalContext(): void public function testDoesNotLogHiddenGlobalContext(): void { - $config = new LoggerConfig(); + $config = new LoggerConfig(); $config->logGlobalContext = true; $logger = new Logger($config); From 2e4ca31977256804d0fb223bd3137ebd35cb0d24 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Thu, 19 Feb 2026 14:43:59 +0530 Subject: [PATCH 07/19] Add docs --- user_guide_src/source/general/context.rst | 370 ++++++++++++++++++ user_guide_src/source/general/context/001.php | 4 + user_guide_src/source/general/context/002.php | 4 + user_guide_src/source/general/context/003.php | 9 + user_guide_src/source/general/context/004.php | 6 + user_guide_src/source/general/context/005.php | 5 + user_guide_src/source/general/context/006.php | 5 + user_guide_src/source/general/context/007.php | 5 + user_guide_src/source/general/context/008.php | 9 + user_guide_src/source/general/context/009.php | 9 + user_guide_src/source/general/context/010.php | 6 + user_guide_src/source/general/context/011.php | 6 + user_guide_src/source/general/context/012.php | 4 + user_guide_src/source/general/context/013.php | 4 + user_guide_src/source/general/context/014.php | 4 + user_guide_src/source/general/context/015.php | 4 + user_guide_src/source/general/context/016.php | 8 + user_guide_src/source/general/context/017.php | 5 + user_guide_src/source/general/context/018.php | 14 + user_guide_src/source/general/context/019.php | 10 + user_guide_src/source/general/context/020.php | 8 + user_guide_src/source/general/context/021.php | 4 + user_guide_src/source/general/context/022.php | 4 + user_guide_src/source/general/context/023.php | 15 + user_guide_src/source/general/context/024.php | 8 + user_guide_src/source/general/index.rst | 1 + 26 files changed, 531 insertions(+) create mode 100644 user_guide_src/source/general/context.rst create mode 100644 user_guide_src/source/general/context/001.php create mode 100644 user_guide_src/source/general/context/002.php create mode 100644 user_guide_src/source/general/context/003.php create mode 100644 user_guide_src/source/general/context/004.php create mode 100644 user_guide_src/source/general/context/005.php create mode 100644 user_guide_src/source/general/context/006.php create mode 100644 user_guide_src/source/general/context/007.php create mode 100644 user_guide_src/source/general/context/008.php create mode 100644 user_guide_src/source/general/context/009.php create mode 100644 user_guide_src/source/general/context/010.php create mode 100644 user_guide_src/source/general/context/011.php create mode 100644 user_guide_src/source/general/context/012.php create mode 100644 user_guide_src/source/general/context/013.php create mode 100644 user_guide_src/source/general/context/014.php create mode 100644 user_guide_src/source/general/context/015.php create mode 100644 user_guide_src/source/general/context/016.php create mode 100644 user_guide_src/source/general/context/017.php create mode 100644 user_guide_src/source/general/context/018.php create mode 100644 user_guide_src/source/general/context/019.php create mode 100644 user_guide_src/source/general/context/020.php create mode 100644 user_guide_src/source/general/context/021.php create mode 100644 user_guide_src/source/general/context/022.php create mode 100644 user_guide_src/source/general/context/023.php create mode 100644 user_guide_src/source/general/context/024.php diff --git a/user_guide_src/source/general/context.rst b/user_guide_src/source/general/context.rst new file mode 100644 index 000000000000..30170b6e54ec --- /dev/null +++ b/user_guide_src/source/general/context.rst @@ -0,0 +1,370 @@ +################### +Context +################### + +.. contents:: + :local: + :depth: 2 + +*********** +What is it? +*********** + +The Context class provides a simple, convenient way to store and retrieve user-defined data throughout a single request. It functions as a key-value store that can hold any data you need to access across different parts of your application during the request lifecycle. + +The Context class is particularly useful for: + +- Storing request-specific metadata (user IDs, request IDs, correlation IDs) +- Passing data between filters, controllers, and other components +- Adding contextual information to your logs automatically +- Storing sensitive data that should not appear in logs + +****************** +Accessing Context +****************** + +You can access the Context service anywhere in your application using the ``service()`` function: + +.. literalinclude:: context/001.php + +********************* +Setting Context Data +********************* + +Setting a Single Value +====================== + +You can store a single key-value pair using the ``set()`` method: + +.. literalinclude:: context/002.php + +Setting Multiple Values +======================= + +You can also set multiple values at once by passing an array: + +.. literalinclude:: context/003.php + +The ``set()`` method returns the Context instance, allowing you to chain multiple calls: + +.. literalinclude:: context/004.php + +********************* +Getting Context Data +********************* + +Retrieving a Single Value +========================== + +Use the ``get()`` method to retrieve a value by its key: + +.. literalinclude:: context/005.php + +You can provide a default value as the second parameter, which will be returned if the key doesn't exist: + +.. literalinclude:: context/006.php + +Retrieving All Data +=================== + +To get all stored context data: + +.. literalinclude:: context/007.php + +Retrieving Specific Keys +========================= + +You can retrieve only specific keys using ``getOnly()``: + +.. literalinclude:: context/008.php + +If you need all data except specific keys, use ``getExcept()``: + +.. literalinclude:: context/009.php + +********************** +Checking for Data +********************** + +You can check if a key exists in the context: + +.. literalinclude:: context/010.php + +Or check if a key is missing (the opposite of ``has()``): + +.. literalinclude:: context/011.php + +********************* +Removing Context Data +********************* + +Removing a Single Value +======================== + +You can remove data from the context using the ``remove()`` method: + +.. literalinclude:: context/012.php + +Removing Multiple Values +========================= + +To remove multiple keys at once, pass an array: + +.. literalinclude:: context/013.php + +Clearing All Data +================= + +To remove all context data: + +.. literalinclude:: context/014.php + +********************* +Hidden Context Data +********************* + +The Context class provides a separate storage area for sensitive data that should not be included in logs. This is useful for storing API keys, passwords, tokens, or other sensitive information that you need to access during the request but don't want to expose in log files. + +Setting Hidden Data +=================== + +Use the ``setHidden()`` method to store sensitive data: + +.. literalinclude:: context/015.php + +You can also set multiple hidden values at once: + +.. literalinclude:: context/016.php + +Getting Hidden Data +=================== + +Retrieve hidden data using ``getHidden()``: + +.. literalinclude:: context/017.php + +The same methods available for regular data also work with hidden data: + +.. literalinclude:: context/018.php + +Checking Hidden Data +==================== + +Check if a hidden key exists: + +.. literalinclude:: context/019.php + +Removing Hidden Data +==================== + +Remove hidden data using ``removeHidden()``: + +.. literalinclude:: context/020.php + +Clearing Hidden Data +==================== + +To clear all hidden data without affecting regular context data: + +.. literalinclude:: context/021.php + +To clear both regular and hidden data: + +.. literalinclude:: context/022.php + +.. important:: Regular data and hidden data are stored separately. A key can exist in both regular and hidden storage with different values. Use ``get()`` for regular data and ``getHidden()`` for hidden data. + +*********************************** +Integration with Logging +*********************************** + +The Context class integrates seamlessly with CodeIgniter's logging system. When enabled, context data is automatically appended to log messages, providing additional information for debugging and monitoring. + +Enabling Global Context Logging +================================ + +To enable automatic logging of context data, set the ``$logGlobalContext`` property to ``true`` in your **app/Config/Logger.php** file: + +.. literalinclude:: context/023.php + +When enabled, all context data (excluding hidden data) will be automatically appended to your log messages as JSON: + +.. literalinclude:: context/024.php + +This would produce a log entry like: + +.. code-block:: text + + ERROR - 2026-02-18 --> Payment processing failed {"user_id":123,"transaction_id":"txn_12345"} + +.. note:: Hidden data set with ``setHidden()`` is **never** included in logs, even when ``$logGlobalContext`` is enabled. This ensures sensitive information like API keys or tokens remain secure. + +*************** +Important Notes +*************** + +- Context data persists only for the duration of a single request. It is not shared between requests. +- The Context service is shared by default, meaning there is one instance per request. +- Hidden data is never included in logs, regardless of the logging configuration. +- Regular context data and hidden context data are stored separately and can have overlapping keys. +- Context is cleared automatically at the end of each request. +- In testing environments, remember to clear context data between tests using ``clearAll()`` to ensure test isolation. + +*************** +Class Reference +*************** + +.. php:namespace:: CodeIgniter\Context + +.. php:class:: Context + + .. php:method:: set($key[, $value = null]) + + :param array|string $key: The key or an array of key-value pairs + :param mixed $value: The value to store (ignored if $key is an array) + :returns: Context instance for method chaining + :rtype: Context + + Sets one or more key-value pairs in the context. + + .. php:method:: setHidden($key[, $value = null]) + + :param array|string $key: The key or an array of key-value pairs + :param mixed $value: The value to store (ignored if $key is an array) + :returns: Context instance for method chaining + :rtype: Context + + Sets one or more key-value pairs in the hidden context. + + .. php:method:: get($key[, $default = null]) + + :param string $key: The key to retrieve + :param mixed $default: Default value if key doesn't exist + :returns: The value or default + :rtype: mixed + + Gets a value from the context. + + .. php:method:: getHidden($key[, $default = null]) + + :param string $key: The key to retrieve + :param mixed $default: Default value if key doesn't exist + :returns: The value or default + :rtype: mixed + + Gets a value from the hidden context. + + .. php:method:: getOnly($keys) + + :param array|string $keys: Key or array of keys to retrieve + :returns: Array of key-value pairs + :rtype: array + + Gets only the specified keys from the context. + + .. php:method:: getOnlyHidden($keys) + + :param array|string $keys: Key or array of keys to retrieve + :returns: Array of key-value pairs + :rtype: array + + Gets only the specified keys from the hidden context. + + .. php:method:: getExcept($keys) + + :param array|string $keys: Key or array of keys to exclude + :returns: Array of key-value pairs + :rtype: array + + Gets all context data except the specified keys. + + .. php:method:: getExceptHidden($keys) + + :param array|string $keys: Key or array of keys to exclude + :returns: Array of key-value pairs + :rtype: array + + Gets all hidden context data except the specified keys. + + .. php:method:: getAll() + + :returns: All context data + :rtype: array + + Gets all data from the context. + + .. php:method:: getAllHidden() + + :returns: All hidden context data + :rtype: array + + Gets all data from the hidden context. + + .. php:method:: has($key) + + :param string $key: The key to check + :returns: True if key exists, false otherwise + :rtype: bool + + Checks if a key exists in the context. + + .. php:method:: hasHidden($key) + + :param string $key: The key to check + :returns: True if key exists, false otherwise + :rtype: bool + + Checks if a key exists in the hidden context. + + .. php:method:: missing($key) + + :param string $key: The key to check + :returns: True if key doesn't exist, false otherwise + :rtype: bool + + Checks if a key doesn't exist in the context. Opposite of ``has()``. + + .. php:method:: missingHidden($key) + + :param string $key: The key to check + :returns: True if key doesn't exist, false otherwise + :rtype: bool + + Checks if a key doesn't exist in the hidden context. Opposite of ``hasHidden()``. + + .. php:method:: remove($key) + + :param array|string $key: The key or array of keys to remove + :returns: Context instance for method chaining + :rtype: Context + + Removes one or more keys from the context. + + .. php:method:: removeHidden($key) + + :param array|string $key: The key or array of keys to remove + :returns: Context instance for method chaining + :rtype: Context + + Removes one or more keys from the hidden context. + + .. php:method:: clear() + + :returns: Context instance for method chaining + :rtype: Context + + Clears all data from the context (does not affect hidden data). + + .. php:method:: clearHidden() + + :returns: Context instance for method chaining + :rtype: Context + + Clears all data from the hidden context (does not affect regular data). + + .. php:method:: clearAll() + + :returns: Context instance for method chaining + :rtype: Context + + Clears all data from both the context and hidden context. diff --git a/user_guide_src/source/general/context/001.php b/user_guide_src/source/general/context/001.php new file mode 100644 index 000000000000..30e6e9d52b36 --- /dev/null +++ b/user_guide_src/source/general/context/001.php @@ -0,0 +1,4 @@ +set('user_id', 123); + diff --git a/user_guide_src/source/general/context/003.php b/user_guide_src/source/general/context/003.php new file mode 100644 index 000000000000..b6eb5fb23b53 --- /dev/null +++ b/user_guide_src/source/general/context/003.php @@ -0,0 +1,9 @@ +set([ + 'user_id' => 123, + 'username' => 'john_doe', + 'request_id' => 'req_abc123', + 'correlation_id' => 'corr_xyz789', +]); + diff --git a/user_guide_src/source/general/context/004.php b/user_guide_src/source/general/context/004.php new file mode 100644 index 000000000000..f68cdc50c198 --- /dev/null +++ b/user_guide_src/source/general/context/004.php @@ -0,0 +1,6 @@ +set('user_id', 123) + ->set('username', 'john_doe') + ->set('request_id', 'req_abc123'); + diff --git a/user_guide_src/source/general/context/005.php b/user_guide_src/source/general/context/005.php new file mode 100644 index 000000000000..5a496c58b501 --- /dev/null +++ b/user_guide_src/source/general/context/005.php @@ -0,0 +1,5 @@ +get('user_id'); +// $userId = 123 + diff --git a/user_guide_src/source/general/context/006.php b/user_guide_src/source/general/context/006.php new file mode 100644 index 000000000000..9703e14d98f7 --- /dev/null +++ b/user_guide_src/source/general/context/006.php @@ -0,0 +1,5 @@ +get('user_role', 'guest'); +// If 'user_role' doesn't exist, $role will be 'guest' + diff --git a/user_guide_src/source/general/context/007.php b/user_guide_src/source/general/context/007.php new file mode 100644 index 000000000000..1112e327b5bb --- /dev/null +++ b/user_guide_src/source/general/context/007.php @@ -0,0 +1,5 @@ +getAll(); +// Returns: ['user_id' => 123, 'username' => 'john_doe', ...] + diff --git a/user_guide_src/source/general/context/008.php b/user_guide_src/source/general/context/008.php new file mode 100644 index 000000000000..070cd5f80db0 --- /dev/null +++ b/user_guide_src/source/general/context/008.php @@ -0,0 +1,9 @@ +getOnly(['user_id', 'username']); +// Returns: ['user_id' => 123, 'username' => 'john_doe'] + +// You can also pass a single key as a string +$userId = $context->getOnly('user_id'); +// Returns: ['user_id' => 123] + diff --git a/user_guide_src/source/general/context/009.php b/user_guide_src/source/general/context/009.php new file mode 100644 index 000000000000..7365457a4d63 --- /dev/null +++ b/user_guide_src/source/general/context/009.php @@ -0,0 +1,9 @@ +getExcept(['password', 'api_key']); +// Returns all data except 'password' and 'api_key' + +// You can also pass a single key as a string +$data = $context->getExcept('password'); +// Returns all data except 'password' + diff --git a/user_guide_src/source/general/context/010.php b/user_guide_src/source/general/context/010.php new file mode 100644 index 000000000000..09541e2b40df --- /dev/null +++ b/user_guide_src/source/general/context/010.php @@ -0,0 +1,6 @@ +has('user_id')) { + // Do something with user_id +} + diff --git a/user_guide_src/source/general/context/011.php b/user_guide_src/source/general/context/011.php new file mode 100644 index 000000000000..a1968abd8746 --- /dev/null +++ b/user_guide_src/source/general/context/011.php @@ -0,0 +1,6 @@ +missing('user_id')) { + // user_id hasn't been set yet +} + diff --git a/user_guide_src/source/general/context/012.php b/user_guide_src/source/general/context/012.php new file mode 100644 index 000000000000..96559acb0dbf --- /dev/null +++ b/user_guide_src/source/general/context/012.php @@ -0,0 +1,4 @@ +remove('user_id'); + diff --git a/user_guide_src/source/general/context/013.php b/user_guide_src/source/general/context/013.php new file mode 100644 index 000000000000..8ce31709a0a4 --- /dev/null +++ b/user_guide_src/source/general/context/013.php @@ -0,0 +1,4 @@ +remove(['user_id', 'username', 'request_id']); + diff --git a/user_guide_src/source/general/context/014.php b/user_guide_src/source/general/context/014.php new file mode 100644 index 000000000000..1fac5ae27cfd --- /dev/null +++ b/user_guide_src/source/general/context/014.php @@ -0,0 +1,4 @@ +clear(); + diff --git a/user_guide_src/source/general/context/015.php b/user_guide_src/source/general/context/015.php new file mode 100644 index 000000000000..dfb657040641 --- /dev/null +++ b/user_guide_src/source/general/context/015.php @@ -0,0 +1,4 @@ +setHidden('api_key', 'sk_live_abc123xyz789'); + diff --git a/user_guide_src/source/general/context/016.php b/user_guide_src/source/general/context/016.php new file mode 100644 index 000000000000..e2d25b392e8f --- /dev/null +++ b/user_guide_src/source/general/context/016.php @@ -0,0 +1,8 @@ +setHidden([ + 'api_key' => 'sk_live_abc123xyz789', + 'api_secret' => 'secret_key_here', + 'db_password' => 'database_password', +]); + diff --git a/user_guide_src/source/general/context/017.php b/user_guide_src/source/general/context/017.php new file mode 100644 index 000000000000..39f304bab02e --- /dev/null +++ b/user_guide_src/source/general/context/017.php @@ -0,0 +1,5 @@ +getHidden('api_key'); +// $apiKey = 'sk_live_abc123xyz789' + diff --git a/user_guide_src/source/general/context/018.php b/user_guide_src/source/general/context/018.php new file mode 100644 index 000000000000..017ef1a5e766 --- /dev/null +++ b/user_guide_src/source/general/context/018.php @@ -0,0 +1,14 @@ +getHidden('api_key', 'default_key'); + +// Get only specific hidden keys +$credentials = $context->getOnlyHidden(['api_key', 'api_secret']); + +// Get all hidden data except specific keys +$data = $context->getExceptHidden(['db_password']); + +// Get all hidden data +$allHidden = $context->getAllHidden(); + diff --git a/user_guide_src/source/general/context/019.php b/user_guide_src/source/general/context/019.php new file mode 100644 index 000000000000..234b6ef39678 --- /dev/null +++ b/user_guide_src/source/general/context/019.php @@ -0,0 +1,10 @@ +hasHidden('api_key')) { + // API key is set +} + +if ($context->missingHidden('api_key')) { + // API key is not set +} + diff --git a/user_guide_src/source/general/context/020.php b/user_guide_src/source/general/context/020.php new file mode 100644 index 000000000000..18ae63a9f588 --- /dev/null +++ b/user_guide_src/source/general/context/020.php @@ -0,0 +1,8 @@ +removeHidden('api_key'); + +// Remove multiple hidden values +$context->removeHidden(['api_key', 'api_secret']); + diff --git a/user_guide_src/source/general/context/021.php b/user_guide_src/source/general/context/021.php new file mode 100644 index 000000000000..b5fb29c0d163 --- /dev/null +++ b/user_guide_src/source/general/context/021.php @@ -0,0 +1,4 @@ +clearHidden(); + diff --git a/user_guide_src/source/general/context/022.php b/user_guide_src/source/general/context/022.php new file mode 100644 index 000000000000..fb90f6adf686 --- /dev/null +++ b/user_guide_src/source/general/context/022.php @@ -0,0 +1,4 @@ +clearAll(); + diff --git a/user_guide_src/source/general/context/023.php b/user_guide_src/source/general/context/023.php new file mode 100644 index 000000000000..3803bed019b1 --- /dev/null +++ b/user_guide_src/source/general/context/023.php @@ -0,0 +1,15 @@ +set('user_id', 123); +$context->set('transaction_id', 'txn_12345'); + +log_message('error', 'Payment processing failed'); + diff --git a/user_guide_src/source/general/index.rst b/user_guide_src/source/general/index.rst index b6f148ca77d4..ca14915a39b4 100644 --- a/user_guide_src/source/general/index.rst +++ b/user_guide_src/source/general/index.rst @@ -12,6 +12,7 @@ General Topics logging errors caching + context ajax modules managing_apps From b055119b7d8607ac3268df152c3ebc160696d2bc Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 21 Feb 2026 15:09:33 +0530 Subject: [PATCH 08/19] add context() helper function --- system/Common.php | 14 ++++++++++++++ tests/system/CommonFunctionsTest.php | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/system/Common.php b/system/Common.php index bcf2a5c14db9..ebafa173afe5 100644 --- a/system/Common.php +++ b/system/Common.php @@ -14,6 +14,7 @@ use CodeIgniter\Cache\CacheInterface; use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\Factories; +use CodeIgniter\Context\Context; use CodeIgniter\Cookie\Cookie; use CodeIgniter\Cookie\CookieStore; use CodeIgniter\Cookie\Exceptions\CookieException; @@ -212,6 +213,19 @@ function config(string $name, bool $getShared = true) } } +if (! function_exists('context')) { + /** + * Provides access to the Context object, which is used to store + * contextual data during a request that can be accessed globally. + * + * @return Context + */ + function context(): Context + { + return service('context'); + } +} + if (! function_exists('cookie')) { /** * Simpler way to create a new Cookie instance. diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index d437f985ebd5..da9574ab4bad 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -821,4 +821,11 @@ public function testRenderBacktrace(): void $this->assertMatchesRegularExpression('/^\s*\d* .+(?:\(\d+\))?: \S+(?:(?:\->|::)\S+)?\(.*\)$/', $render); } } + + public function testContext(): void + { + service('context')->set('foo', 'bar'); + + $this->assertSame('bar', context()->get('foo')); + } } From c60b9f9add08b4c556209fdeaaa18d746fe1857e Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 21 Feb 2026 15:13:40 +0530 Subject: [PATCH 09/19] cs-fix and fixing static analysis problems --- system/Common.php | 2 -- system/Context/Context.php | 10 ++++++---- tests/system/Context/ContextTest.php | 2 ++ user_guide_src/source/general/context/001.php | 1 - user_guide_src/source/general/context/002.php | 1 - user_guide_src/source/general/context/003.php | 1 - user_guide_src/source/general/context/004.php | 1 - user_guide_src/source/general/context/005.php | 1 - user_guide_src/source/general/context/006.php | 1 - user_guide_src/source/general/context/007.php | 1 - user_guide_src/source/general/context/008.php | 1 - user_guide_src/source/general/context/009.php | 1 - user_guide_src/source/general/context/010.php | 1 - user_guide_src/source/general/context/011.php | 1 - user_guide_src/source/general/context/012.php | 1 - user_guide_src/source/general/context/013.php | 1 - user_guide_src/source/general/context/014.php | 1 - user_guide_src/source/general/context/015.php | 1 - user_guide_src/source/general/context/016.php | 7 +++---- user_guide_src/source/general/context/017.php | 1 - user_guide_src/source/general/context/018.php | 1 - user_guide_src/source/general/context/019.php | 1 - user_guide_src/source/general/context/020.php | 1 - user_guide_src/source/general/context/021.php | 1 - user_guide_src/source/general/context/022.php | 1 - user_guide_src/source/general/context/023.php | 1 - user_guide_src/source/general/context/024.php | 1 - 27 files changed, 11 insertions(+), 33 deletions(-) diff --git a/system/Common.php b/system/Common.php index ebafa173afe5..04115993ef87 100644 --- a/system/Common.php +++ b/system/Common.php @@ -217,8 +217,6 @@ function config(string $name, bool $getShared = true) /** * Provides access to the Context object, which is used to store * contextual data during a request that can be accessed globally. - * - * @return Context */ function context(): Context { diff --git a/system/Context/Context.php b/system/Context/Context.php index 1ff836226965..24852b14a8eb 100644 --- a/system/Context/Context.php +++ b/system/Context/Context.php @@ -1,5 +1,7 @@ data, static fn ($k) => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return array_filter($this->data, static fn ($k): bool => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** @@ -120,7 +122,7 @@ public function getExcept(array|string $keys): array $keys = [$keys]; } - return array_filter($this->data, static fn ($k) => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return array_filter($this->data, static fn ($k): bool => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** @@ -159,7 +161,7 @@ public function getOnlyHidden(array|string $keys): array $keys = [$keys]; } - return array_filter($this->hiddenData, static fn ($k) => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return array_filter($this->hiddenData, static fn ($k): bool => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** @@ -175,7 +177,7 @@ public function getExceptHidden(array|string $keys): array $keys = [$keys]; } - return array_filter($this->hiddenData, static fn ($k) => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return array_filter($this->hiddenData, static fn ($k): bool => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); } /** diff --git a/tests/system/Context/ContextTest.php b/tests/system/Context/ContextTest.php index 09f50b8561d9..c315519d81b4 100644 --- a/tests/system/Context/ContextTest.php +++ b/tests/system/Context/ContextTest.php @@ -1,5 +1,7 @@ set('user_id', 123); - diff --git a/user_guide_src/source/general/context/003.php b/user_guide_src/source/general/context/003.php index b6eb5fb23b53..60d2ca87ee2f 100644 --- a/user_guide_src/source/general/context/003.php +++ b/user_guide_src/source/general/context/003.php @@ -6,4 +6,3 @@ 'request_id' => 'req_abc123', 'correlation_id' => 'corr_xyz789', ]); - diff --git a/user_guide_src/source/general/context/004.php b/user_guide_src/source/general/context/004.php index f68cdc50c198..29dd1e38cafc 100644 --- a/user_guide_src/source/general/context/004.php +++ b/user_guide_src/source/general/context/004.php @@ -3,4 +3,3 @@ $context->set('user_id', 123) ->set('username', 'john_doe') ->set('request_id', 'req_abc123'); - diff --git a/user_guide_src/source/general/context/005.php b/user_guide_src/source/general/context/005.php index 5a496c58b501..1ddd78820180 100644 --- a/user_guide_src/source/general/context/005.php +++ b/user_guide_src/source/general/context/005.php @@ -2,4 +2,3 @@ $userId = $context->get('user_id'); // $userId = 123 - diff --git a/user_guide_src/source/general/context/006.php b/user_guide_src/source/general/context/006.php index 9703e14d98f7..c56335f6bc27 100644 --- a/user_guide_src/source/general/context/006.php +++ b/user_guide_src/source/general/context/006.php @@ -2,4 +2,3 @@ $role = $context->get('user_role', 'guest'); // If 'user_role' doesn't exist, $role will be 'guest' - diff --git a/user_guide_src/source/general/context/007.php b/user_guide_src/source/general/context/007.php index 1112e327b5bb..9ad340998671 100644 --- a/user_guide_src/source/general/context/007.php +++ b/user_guide_src/source/general/context/007.php @@ -2,4 +2,3 @@ $allData = $context->getAll(); // Returns: ['user_id' => 123, 'username' => 'john_doe', ...] - diff --git a/user_guide_src/source/general/context/008.php b/user_guide_src/source/general/context/008.php index 070cd5f80db0..519ecf64e174 100644 --- a/user_guide_src/source/general/context/008.php +++ b/user_guide_src/source/general/context/008.php @@ -6,4 +6,3 @@ // You can also pass a single key as a string $userId = $context->getOnly('user_id'); // Returns: ['user_id' => 123] - diff --git a/user_guide_src/source/general/context/009.php b/user_guide_src/source/general/context/009.php index 7365457a4d63..2c5158dd36b0 100644 --- a/user_guide_src/source/general/context/009.php +++ b/user_guide_src/source/general/context/009.php @@ -6,4 +6,3 @@ // You can also pass a single key as a string $data = $context->getExcept('password'); // Returns all data except 'password' - diff --git a/user_guide_src/source/general/context/010.php b/user_guide_src/source/general/context/010.php index 09541e2b40df..61b10e76a4c5 100644 --- a/user_guide_src/source/general/context/010.php +++ b/user_guide_src/source/general/context/010.php @@ -3,4 +3,3 @@ if ($context->has('user_id')) { // Do something with user_id } - diff --git a/user_guide_src/source/general/context/011.php b/user_guide_src/source/general/context/011.php index a1968abd8746..534b4e32e388 100644 --- a/user_guide_src/source/general/context/011.php +++ b/user_guide_src/source/general/context/011.php @@ -3,4 +3,3 @@ if ($context->missing('user_id')) { // user_id hasn't been set yet } - diff --git a/user_guide_src/source/general/context/012.php b/user_guide_src/source/general/context/012.php index 96559acb0dbf..2c6c3598ab91 100644 --- a/user_guide_src/source/general/context/012.php +++ b/user_guide_src/source/general/context/012.php @@ -1,4 +1,3 @@ remove('user_id'); - diff --git a/user_guide_src/source/general/context/013.php b/user_guide_src/source/general/context/013.php index 8ce31709a0a4..5922fd6605ac 100644 --- a/user_guide_src/source/general/context/013.php +++ b/user_guide_src/source/general/context/013.php @@ -1,4 +1,3 @@ remove(['user_id', 'username', 'request_id']); - diff --git a/user_guide_src/source/general/context/014.php b/user_guide_src/source/general/context/014.php index 1fac5ae27cfd..18746e68281d 100644 --- a/user_guide_src/source/general/context/014.php +++ b/user_guide_src/source/general/context/014.php @@ -1,4 +1,3 @@ clear(); - diff --git a/user_guide_src/source/general/context/015.php b/user_guide_src/source/general/context/015.php index dfb657040641..560f233a600a 100644 --- a/user_guide_src/source/general/context/015.php +++ b/user_guide_src/source/general/context/015.php @@ -1,4 +1,3 @@ setHidden('api_key', 'sk_live_abc123xyz789'); - diff --git a/user_guide_src/source/general/context/016.php b/user_guide_src/source/general/context/016.php index e2d25b392e8f..b7b3a906ac78 100644 --- a/user_guide_src/source/general/context/016.php +++ b/user_guide_src/source/general/context/016.php @@ -1,8 +1,7 @@ setHidden([ - 'api_key' => 'sk_live_abc123xyz789', - 'api_secret' => 'secret_key_here', - 'db_password' => 'database_password', + 'api_key' => 'sk_live_abc123xyz789', + 'api_secret' => 'secret_key_here', + 'db_password' => 'database_password', ]); - diff --git a/user_guide_src/source/general/context/017.php b/user_guide_src/source/general/context/017.php index 39f304bab02e..e26ceda619ff 100644 --- a/user_guide_src/source/general/context/017.php +++ b/user_guide_src/source/general/context/017.php @@ -2,4 +2,3 @@ $apiKey = $context->getHidden('api_key'); // $apiKey = 'sk_live_abc123xyz789' - diff --git a/user_guide_src/source/general/context/018.php b/user_guide_src/source/general/context/018.php index 017ef1a5e766..9c52af5e6b05 100644 --- a/user_guide_src/source/general/context/018.php +++ b/user_guide_src/source/general/context/018.php @@ -11,4 +11,3 @@ // Get all hidden data $allHidden = $context->getAllHidden(); - diff --git a/user_guide_src/source/general/context/019.php b/user_guide_src/source/general/context/019.php index 234b6ef39678..4e55c610538f 100644 --- a/user_guide_src/source/general/context/019.php +++ b/user_guide_src/source/general/context/019.php @@ -7,4 +7,3 @@ if ($context->missingHidden('api_key')) { // API key is not set } - diff --git a/user_guide_src/source/general/context/020.php b/user_guide_src/source/general/context/020.php index 18ae63a9f588..a06a3f6d9c3b 100644 --- a/user_guide_src/source/general/context/020.php +++ b/user_guide_src/source/general/context/020.php @@ -5,4 +5,3 @@ // Remove multiple hidden values $context->removeHidden(['api_key', 'api_secret']); - diff --git a/user_guide_src/source/general/context/021.php b/user_guide_src/source/general/context/021.php index b5fb29c0d163..7fca9aa5869f 100644 --- a/user_guide_src/source/general/context/021.php +++ b/user_guide_src/source/general/context/021.php @@ -1,4 +1,3 @@ clearHidden(); - diff --git a/user_guide_src/source/general/context/022.php b/user_guide_src/source/general/context/022.php index fb90f6adf686..bc26dead4e00 100644 --- a/user_guide_src/source/general/context/022.php +++ b/user_guide_src/source/general/context/022.php @@ -1,4 +1,3 @@ clearAll(); - diff --git a/user_guide_src/source/general/context/023.php b/user_guide_src/source/general/context/023.php index 3803bed019b1..7e9f878c30bb 100644 --- a/user_guide_src/source/general/context/023.php +++ b/user_guide_src/source/general/context/023.php @@ -12,4 +12,3 @@ class Logger extends BaseConfig // ... } - diff --git a/user_guide_src/source/general/context/024.php b/user_guide_src/source/general/context/024.php index 1afc0dad17f3..cb730f8d20fc 100644 --- a/user_guide_src/source/general/context/024.php +++ b/user_guide_src/source/general/context/024.php @@ -5,4 +5,3 @@ $context->set('transaction_id', 'txn_12345'); log_message('error', 'Payment processing failed'); - From 66fd4aea03e83037f921a68a851a592cac519ba5 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 21 Feb 2026 15:30:02 +0530 Subject: [PATCH 10/19] Some doc fixing --- user_guide_src/source/changelogs/v4.8.0.rst | 2 ++ user_guide_src/source/general/context.rst | 26 ++++++++++++------ user_guide_src/source/general/logging.rst | 29 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index b45b1fda6b16..5404ace41547 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -74,6 +74,8 @@ Model Libraries ========= +- **Context**: This new feature allows you to easily set and retrieve normal or hidden contextual data for the current request. See :ref:`Context ` for details. + Helpers and Functions ===================== diff --git a/user_guide_src/source/general/context.rst b/user_guide_src/source/general/context.rst index 30170b6e54ec..db813c14d106 100644 --- a/user_guide_src/source/general/context.rst +++ b/user_guide_src/source/general/context.rst @@ -1,7 +1,11 @@ +.. _context: + ################### Context ################### +.. versionadded:: 4.8.0 + .. contents:: :local: :depth: 2 @@ -19,9 +23,9 @@ The Context class is particularly useful for: - Adding contextual information to your logs automatically - Storing sensitive data that should not appear in logs -****************** -Accessing Context -****************** +*********************** +Accessing Context Class +*********************** You can access the Context service anywhere in your application using the ``service()`` function: @@ -123,7 +127,9 @@ To remove all context data: Hidden Context Data ********************* -The Context class provides a separate storage area for sensitive data that should not be included in logs. This is useful for storing API keys, passwords, tokens, or other sensitive information that you need to access during the request but don't want to expose in log files. +The Context class provides a separate storage area for sensitive data that should not be included in logs. +This is useful for storing API keys, passwords, tokens, or other sensitive information that you need to access +during the request but don't want to expose in log files. Setting Hidden Data =================== @@ -172,18 +178,21 @@ To clear both regular and hidden data: .. literalinclude:: context/022.php -.. important:: Regular data and hidden data are stored separately. A key can exist in both regular and hidden storage with different values. Use ``get()`` for regular data and ``getHidden()`` for hidden data. +.. important:: Regular data and hidden data are stored separately. A key can exist in both regular and hidden storage +with different values. Use ``get()`` for regular data and ``getHidden()`` for hidden data. *********************************** Integration with Logging *********************************** -The Context class integrates seamlessly with CodeIgniter's logging system. When enabled, context data is automatically appended to log messages, providing additional information for debugging and monitoring. +The Context class integrates seamlessly with CodeIgniter's logging system. When enabled, context data is automatically +appended to log messages, providing additional information for debugging and monitoring. Enabling Global Context Logging ================================ -To enable automatic logging of context data, set the ``$logGlobalContext`` property to ``true`` in your **app/Config/Logger.php** file: +To enable automatic logging of context data, set the ``$logGlobalContext`` property to ``true`` in your +**app/Config/Logger.php** file: .. literalinclude:: context/023.php @@ -197,7 +206,8 @@ This would produce a log entry like: ERROR - 2026-02-18 --> Payment processing failed {"user_id":123,"transaction_id":"txn_12345"} -.. note:: Hidden data set with ``setHidden()`` is **never** included in logs, even when ``$logGlobalContext`` is enabled. This ensures sensitive information like API keys or tokens remain secure. +.. note:: Hidden data set with ``setHidden()`` is **never** included in logs, even when ``$logGlobalContext`` is enabled. +This ensures sensitive information like API keys or tokens remain secure. *************** Important Notes diff --git a/user_guide_src/source/general/logging.rst b/user_guide_src/source/general/logging.rst index 6e159827b1e9..5f21ad7888b8 100644 --- a/user_guide_src/source/general/logging.rst +++ b/user_guide_src/source/general/logging.rst @@ -118,6 +118,35 @@ Several core placeholders exist that will be automatically expanded for you base | {env:foo} | The value of 'foo' in $_ENV | +----------------+---------------------------------------------------+ +.. _logging-global-context: + +Global Context Logging +---------------------- + +.. versionadded:: 4.8.0 + +You can automatically append context data to all log messages by enabling the ``$logGlobalContext`` +property in **app/Config/Logger.php**: + +.. literalinclude:: context/023.php + +When enabled, all regular context data (set via the :ref:`Context class `) is automatically +appended to every log message as a JSON string: + +.. literalinclude:: context/024.php + +This would produce a log entry like: + +.. code-block:: text + + ERROR - 2026-02-18 --> Payment processing failed {"user_id":123,"transaction_id":"txn_12345"} + +.. note:: Hidden data set with ``setHidden()`` is **never** included in log output, even when + ``$logGlobalContext`` is enabled. This protects sensitive information such as API keys + and tokens from appearing in log files. + +See :ref:`context` for full documentation on storing and managing context data. + Using Third-Party Loggers ========================= From daf6d6b5a7fbaff41c31c36150be4eebc0b9bd6c Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 21 Feb 2026 15:50:10 +0530 Subject: [PATCH 11/19] Fix rst errors. --- user_guide_src/source/general/context.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/general/context.rst b/user_guide_src/source/general/context.rst index db813c14d106..17dc17a502fc 100644 --- a/user_guide_src/source/general/context.rst +++ b/user_guide_src/source/general/context.rst @@ -178,8 +178,7 @@ To clear both regular and hidden data: .. literalinclude:: context/022.php -.. important:: Regular data and hidden data are stored separately. A key can exist in both regular and hidden storage -with different values. Use ``get()`` for regular data and ``getHidden()`` for hidden data. +.. important:: Regular data and hidden data are stored separately. A key can exist in both regular and hidden storage with different values. Use ``get()`` for regular data and ``getHidden()`` for hidden data. *********************************** Integration with Logging @@ -206,8 +205,7 @@ This would produce a log entry like: ERROR - 2026-02-18 --> Payment processing failed {"user_id":123,"transaction_id":"txn_12345"} -.. note:: Hidden data set with ``setHidden()`` is **never** included in logs, even when ``$logGlobalContext`` is enabled. -This ensures sensitive information like API keys or tokens remain secure. +.. note:: Hidden data set with ``setHidden()`` is **never** included in logs, even when ``$logGlobalContext`` is enabled. This ensures sensitive information like API keys or tokens remain secure. *************** Important Notes From 1136fa0f7af7d4c5466976bf8d596098d70d4634 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sun, 22 Feb 2026 15:33:59 +0530 Subject: [PATCH 12/19] Apply suggested changes --- app/Config/Logger.php | 2 +- system/Context/Context.php | 35 ++++--------------- system/Log/Logger.php | 6 ++-- tests/system/Context/ContextTest.php | 18 ---------- user_guide_src/source/general/context.rst | 30 +++++++--------- user_guide_src/source/general/context/011.php | 4 +-- user_guide_src/source/general/context/012.php | 2 +- user_guide_src/source/general/context/013.php | 2 +- user_guide_src/source/general/context/014.php | 2 +- user_guide_src/source/general/context/015.php | 6 +++- user_guide_src/source/general/context/016.php | 7 ++-- user_guide_src/source/general/context/017.php | 13 +++++-- user_guide_src/source/general/context/018.php | 14 ++------ user_guide_src/source/general/context/019.php | 10 +++--- user_guide_src/source/general/context/020.php | 6 +--- user_guide_src/source/general/context/021.php | 2 +- user_guide_src/source/general/context/022.php | 13 ++++++- user_guide_src/source/general/context/023.php | 15 +++----- user_guide_src/source/general/context/024.php | 7 ---- 19 files changed, 71 insertions(+), 123 deletions(-) delete mode 100644 user_guide_src/source/general/context/024.php diff --git a/app/Config/Logger.php b/app/Config/Logger.php index e303965a397b..16f2fa9aa890 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -65,7 +65,7 @@ class Logger extends BaseConfig * **NOTE:** This **DOES NOT** include any data that has been marked as hidden * using the `setHidden()` method of the Context class. */ - public bool $logGlobalContext = false; + public bool $logGlobalContext = true; /** * -------------------------------------------------------------------------- diff --git a/system/Context/Context.php b/system/Context/Context.php index 24852b14a8eb..2cd29957c8a5 100644 --- a/system/Context/Context.php +++ b/system/Context/Context.php @@ -13,6 +13,8 @@ namespace CodeIgniter\Context; +use CodeIgniter\Helpers\Array\ArrayHelper; + class Context { /** @@ -82,6 +84,7 @@ public function setHidden(array|string $key, mixed $value = null): self /** * Get a value from the context by its key, or return a default value if the key does not exist. + * Supports dot notation for nested arrays (e.g., 'user.profile.name' to access $data['user']['profile']['name']). * * @param string $key The key to identify the data. * @param mixed|null $default The default value to return if the key does not exist in the context. @@ -90,7 +93,7 @@ public function setHidden(array|string $key, mixed $value = null): self */ public function get(string $key, mixed $default = null): mixed { - return $this->data[$key] ?? $default; + return ArrayHelper::dotSearch($key, $this->data) ?? $default; } /** @@ -145,7 +148,7 @@ public function getAll(): array */ public function getHidden(string $key, mixed $default = null): mixed { - return $this->hiddenData[$key] ?? $default; + return ArrayHelper::dotSearch($key, $this->hiddenData) ?? $default; } /** @@ -190,18 +193,6 @@ public function getAllHidden(): array return $this->hiddenData; } - /** - * Check if a key does not exist in the context. Exactly the opposite of `has()`. - * - * @param string $key The key to check for non-existence in the context. - * - * @return bool True if the key does not exist in the context, false otherwise. - */ - public function missing(string $key): bool - { - return ! $this->has($key); - } - /** * Check if a key exists in the context. * @@ -211,19 +202,7 @@ public function missing(string $key): bool */ public function has(string $key): bool { - return array_key_exists($key, $this->data); - } - - /** - * Check if a key does not exist in the hidden context. Exactly the opposite of `hasHidden()`. - * - * @param string $key The key to check for non-existence in the hidden context. - * - * @return bool True if the key does not exist in the hidden context, false otherwise. - */ - public function missingHidden(string $key): bool - { - return ! $this->hasHidden($key); + return ArrayHelper::dotKeyExists($key, $this->data); } /** @@ -235,7 +214,7 @@ public function missingHidden(string $key): bool */ public function hasHidden(string $key): bool { - return array_key_exists($key, $this->hiddenData); + return ArrayHelper::dotKeyExists($key, $this->hiddenData); } /** diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 22eb3de48637..1bb94e95ee26 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -121,7 +121,7 @@ class Logger implements LoggerInterface * * @var bool */ - protected $logGlobalContext; + protected bool $logGlobalContext = true; /** * Constructor. @@ -164,7 +164,7 @@ public function __construct($config, bool $debug = CI_DEBUG) $this->logCache = []; } - $this->logGlobalContext = $config->logGlobalContext; + $this->logGlobalContext = $config->logGlobalContext ?? $this->logGlobalContext; } /** @@ -265,7 +265,7 @@ public function log($level, string|Stringable $message, array $context = []): vo if ($this->logGlobalContext) { $globalContext = service('context')->getAll(); - if (is_array($globalContext) && $globalContext !== []) { + if ($globalContext !== []) { $message .= ' ' . json_encode($globalContext); } } diff --git a/tests/system/Context/ContextTest.php b/tests/system/Context/ContextTest.php index c315519d81b4..dacb6615fbfd 100644 --- a/tests/system/Context/ContextTest.php +++ b/tests/system/Context/ContextTest.php @@ -328,15 +328,6 @@ public function testHasKey(): void $this->assertTrue($context->has('user_id')); } - public function testMissingKey(): void - { - $context = service('context'); - $this->assertTrue($context->missing('user_id')); - - $context->set('user_id', 123); - $this->assertFalse($context->missing('user_id')); - } - public function testHasHiddenKey(): void { $context = service('context'); @@ -346,15 +337,6 @@ public function testHasHiddenKey(): void $this->assertTrue($context->hasHidden('api_key')); } - public function testMissingHiddenKey(): void - { - $context = service('context'); - $this->assertTrue($context->missingHidden('api_key')); - - $context->setHidden('api_key', 'secret'); - $this->assertFalse($context->missingHidden('api_key')); - } - public function testRemoveSingleValue(): void { $context = service('context'); diff --git a/user_guide_src/source/general/context.rst b/user_guide_src/source/general/context.rst index 17dc17a502fc..bee097946596 100644 --- a/user_guide_src/source/general/context.rst +++ b/user_guide_src/source/general/context.rst @@ -94,10 +94,6 @@ You can check if a key exists in the context: .. literalinclude:: context/010.php -Or check if a key is missing (the opposite of ``has()``): - -.. literalinclude:: context/011.php - ********************* Removing Context Data ********************* @@ -107,21 +103,21 @@ Removing a Single Value You can remove data from the context using the ``remove()`` method: -.. literalinclude:: context/012.php +.. literalinclude:: context/011.php Removing Multiple Values ========================= To remove multiple keys at once, pass an array: -.. literalinclude:: context/013.php +.. literalinclude:: context/012.php Clearing All Data ================= To remove all context data: -.. literalinclude:: context/014.php +.. literalinclude:: context/013.php ********************* Hidden Context Data @@ -136,47 +132,47 @@ Setting Hidden Data Use the ``setHidden()`` method to store sensitive data: -.. literalinclude:: context/015.php +.. literalinclude:: context/014.php You can also set multiple hidden values at once: -.. literalinclude:: context/016.php +.. literalinclude:: context/015.php Getting Hidden Data =================== Retrieve hidden data using ``getHidden()``: -.. literalinclude:: context/017.php +.. literalinclude:: context/016.php The same methods available for regular data also work with hidden data: -.. literalinclude:: context/018.php +.. literalinclude:: context/017.php Checking Hidden Data ==================== Check if a hidden key exists: -.. literalinclude:: context/019.php +.. literalinclude:: context/018.php Removing Hidden Data ==================== Remove hidden data using ``removeHidden()``: -.. literalinclude:: context/020.php +.. literalinclude:: context/019.php Clearing Hidden Data ==================== To clear all hidden data without affecting regular context data: -.. literalinclude:: context/021.php +.. literalinclude:: context/020.php To clear both regular and hidden data: -.. literalinclude:: context/022.php +.. literalinclude:: context/021.php .. important:: Regular data and hidden data are stored separately. A key can exist in both regular and hidden storage with different values. Use ``get()`` for regular data and ``getHidden()`` for hidden data. @@ -193,11 +189,11 @@ Enabling Global Context Logging To enable automatic logging of context data, set the ``$logGlobalContext`` property to ``true`` in your **app/Config/Logger.php** file: -.. literalinclude:: context/023.php +.. literalinclude:: context/022.php When enabled, all context data (excluding hidden data) will be automatically appended to your log messages as JSON: -.. literalinclude:: context/024.php +.. literalinclude:: context/023.php This would produce a log entry like: diff --git a/user_guide_src/source/general/context/011.php b/user_guide_src/source/general/context/011.php index 534b4e32e388..2c6c3598ab91 100644 --- a/user_guide_src/source/general/context/011.php +++ b/user_guide_src/source/general/context/011.php @@ -1,5 +1,3 @@ missing('user_id')) { - // user_id hasn't been set yet -} +$context->remove('user_id'); diff --git a/user_guide_src/source/general/context/012.php b/user_guide_src/source/general/context/012.php index 2c6c3598ab91..5922fd6605ac 100644 --- a/user_guide_src/source/general/context/012.php +++ b/user_guide_src/source/general/context/012.php @@ -1,3 +1,3 @@ remove('user_id'); +$context->remove(['user_id', 'username', 'request_id']); diff --git a/user_guide_src/source/general/context/013.php b/user_guide_src/source/general/context/013.php index 5922fd6605ac..18746e68281d 100644 --- a/user_guide_src/source/general/context/013.php +++ b/user_guide_src/source/general/context/013.php @@ -1,3 +1,3 @@ remove(['user_id', 'username', 'request_id']); +$context->clear(); diff --git a/user_guide_src/source/general/context/014.php b/user_guide_src/source/general/context/014.php index 18746e68281d..560f233a600a 100644 --- a/user_guide_src/source/general/context/014.php +++ b/user_guide_src/source/general/context/014.php @@ -1,3 +1,3 @@ clear(); +$context->setHidden('api_key', 'sk_live_abc123xyz789'); diff --git a/user_guide_src/source/general/context/015.php b/user_guide_src/source/general/context/015.php index 560f233a600a..b7b3a906ac78 100644 --- a/user_guide_src/source/general/context/015.php +++ b/user_guide_src/source/general/context/015.php @@ -1,3 +1,7 @@ setHidden('api_key', 'sk_live_abc123xyz789'); +$context->setHidden([ + 'api_key' => 'sk_live_abc123xyz789', + 'api_secret' => 'secret_key_here', + 'db_password' => 'database_password', +]); diff --git a/user_guide_src/source/general/context/016.php b/user_guide_src/source/general/context/016.php index b7b3a906ac78..e26ceda619ff 100644 --- a/user_guide_src/source/general/context/016.php +++ b/user_guide_src/source/general/context/016.php @@ -1,7 +1,4 @@ setHidden([ - 'api_key' => 'sk_live_abc123xyz789', - 'api_secret' => 'secret_key_here', - 'db_password' => 'database_password', -]); +$apiKey = $context->getHidden('api_key'); +// $apiKey = 'sk_live_abc123xyz789' diff --git a/user_guide_src/source/general/context/017.php b/user_guide_src/source/general/context/017.php index e26ceda619ff..9c52af5e6b05 100644 --- a/user_guide_src/source/general/context/017.php +++ b/user_guide_src/source/general/context/017.php @@ -1,4 +1,13 @@ getHidden('api_key'); -// $apiKey = 'sk_live_abc123xyz789' +// Get with default value +$apiKey = $context->getHidden('api_key', 'default_key'); + +// Get only specific hidden keys +$credentials = $context->getOnlyHidden(['api_key', 'api_secret']); + +// Get all hidden data except specific keys +$data = $context->getExceptHidden(['db_password']); + +// Get all hidden data +$allHidden = $context->getAllHidden(); diff --git a/user_guide_src/source/general/context/018.php b/user_guide_src/source/general/context/018.php index 9c52af5e6b05..b5084a23f143 100644 --- a/user_guide_src/source/general/context/018.php +++ b/user_guide_src/source/general/context/018.php @@ -1,13 +1,5 @@ getHidden('api_key', 'default_key'); - -// Get only specific hidden keys -$credentials = $context->getOnlyHidden(['api_key', 'api_secret']); - -// Get all hidden data except specific keys -$data = $context->getExceptHidden(['db_password']); - -// Get all hidden data -$allHidden = $context->getAllHidden(); +if ($context->hasHidden('api_key')) { + // API key is set +} diff --git a/user_guide_src/source/general/context/019.php b/user_guide_src/source/general/context/019.php index 4e55c610538f..a06a3f6d9c3b 100644 --- a/user_guide_src/source/general/context/019.php +++ b/user_guide_src/source/general/context/019.php @@ -1,9 +1,7 @@ hasHidden('api_key')) { - // API key is set -} +// Remove a single hidden value +$context->removeHidden('api_key'); -if ($context->missingHidden('api_key')) { - // API key is not set -} +// Remove multiple hidden values +$context->removeHidden(['api_key', 'api_secret']); diff --git a/user_guide_src/source/general/context/020.php b/user_guide_src/source/general/context/020.php index a06a3f6d9c3b..7fca9aa5869f 100644 --- a/user_guide_src/source/general/context/020.php +++ b/user_guide_src/source/general/context/020.php @@ -1,7 +1,3 @@ removeHidden('api_key'); - -// Remove multiple hidden values -$context->removeHidden(['api_key', 'api_secret']); +$context->clearHidden(); diff --git a/user_guide_src/source/general/context/021.php b/user_guide_src/source/general/context/021.php index 7fca9aa5869f..bc26dead4e00 100644 --- a/user_guide_src/source/general/context/021.php +++ b/user_guide_src/source/general/context/021.php @@ -1,3 +1,3 @@ clearHidden(); +$context->clearAll(); diff --git a/user_guide_src/source/general/context/022.php b/user_guide_src/source/general/context/022.php index bc26dead4e00..7e9f878c30bb 100644 --- a/user_guide_src/source/general/context/022.php +++ b/user_guide_src/source/general/context/022.php @@ -1,3 +1,14 @@ clearAll(); +namespace Config; + +use CodeIgniter\Config\BaseConfig; + +class Logger extends BaseConfig +{ + // ... + + public bool $logGlobalContext = true; + + // ... +} diff --git a/user_guide_src/source/general/context/023.php b/user_guide_src/source/general/context/023.php index 7e9f878c30bb..cb730f8d20fc 100644 --- a/user_guide_src/source/general/context/023.php +++ b/user_guide_src/source/general/context/023.php @@ -1,14 +1,7 @@ set('user_id', 123); +$context->set('transaction_id', 'txn_12345'); -use CodeIgniter\Config\BaseConfig; - -class Logger extends BaseConfig -{ - // ... - - public bool $logGlobalContext = true; - - // ... -} +log_message('error', 'Payment processing failed'); diff --git a/user_guide_src/source/general/context/024.php b/user_guide_src/source/general/context/024.php deleted file mode 100644 index cb730f8d20fc..000000000000 --- a/user_guide_src/source/general/context/024.php +++ /dev/null @@ -1,7 +0,0 @@ -set('user_id', 123); -$context->set('transaction_id', 'txn_12345'); - -log_message('error', 'Payment processing failed'); From 07f895e31fbf1b226641f452f33e60d15061872a Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Mon, 23 Feb 2026 15:02:56 +0530 Subject: [PATCH 13/19] Set $logGlobalContext to false by default --- app/Config/Logger.php | 2 +- system/Log/Logger.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Config/Logger.php b/app/Config/Logger.php index 16f2fa9aa890..e303965a397b 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -65,7 +65,7 @@ class Logger extends BaseConfig * **NOTE:** This **DOES NOT** include any data that has been marked as hidden * using the `setHidden()` method of the Context class. */ - public bool $logGlobalContext = true; + public bool $logGlobalContext = false; /** * -------------------------------------------------------------------------- diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 1bb94e95ee26..85f7a0f1e9a4 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -121,7 +121,7 @@ class Logger implements LoggerInterface * * @var bool */ - protected bool $logGlobalContext = true; + protected bool $logGlobalContext = false; /** * Constructor. From 7bf481aa105cbd813dd537f9555e6e6ebc725bc9 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Mon, 23 Feb 2026 15:04:12 +0530 Subject: [PATCH 14/19] Add docs for helper method --- user_guide_src/source/general/context.rst | 2 +- user_guide_src/source/general/context/001.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/general/context.rst b/user_guide_src/source/general/context.rst index bee097946596..5c9dac35b16b 100644 --- a/user_guide_src/source/general/context.rst +++ b/user_guide_src/source/general/context.rst @@ -27,7 +27,7 @@ The Context class is particularly useful for: Accessing Context Class *********************** -You can access the Context service anywhere in your application using the ``service()`` function: +You can access the Context service anywhere in your application using the ``service()`` function or ``context()`` helper: .. literalinclude:: context/001.php diff --git a/user_guide_src/source/general/context/001.php b/user_guide_src/source/general/context/001.php index f424ec9dc35f..53004b9f25c5 100644 --- a/user_guide_src/source/general/context/001.php +++ b/user_guide_src/source/general/context/001.php @@ -1,3 +1,6 @@ Date: Mon, 23 Feb 2026 15:09:28 +0530 Subject: [PATCH 15/19] cs-fix --- system/Log/Logger.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 85f7a0f1e9a4..87edf8fbc14c 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -118,8 +118,6 @@ class Logger implements LoggerInterface * Whether to log the global context data. * * Set in app/Config/Logger.php - * - * @var bool */ protected bool $logGlobalContext = false; From 2f11d7c792ec8d8991a92266ad4e264cf57c5046 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Mon, 23 Feb 2026 15:15:24 +0530 Subject: [PATCH 16/19] Fix the doc issue --- user_guide_src/source/general/logging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/general/logging.rst b/user_guide_src/source/general/logging.rst index 5f21ad7888b8..1c2fc453eb64 100644 --- a/user_guide_src/source/general/logging.rst +++ b/user_guide_src/source/general/logging.rst @@ -133,7 +133,7 @@ property in **app/Config/Logger.php**: When enabled, all regular context data (set via the :ref:`Context class `) is automatically appended to every log message as a JSON string: -.. literalinclude:: context/024.php +.. literalinclude:: context/023.php This would produce a log entry like: From f36449e191e33fb44b172c6dd823c949153c8431 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 24 Feb 2026 16:36:55 +0530 Subject: [PATCH 17/19] Applying code suggestions Co-authored-by: John Paul E. Balandan, CPA Co-authored-by: michalsn --- system/Config/Services.php | 4 +- system/Context/Context.php | 99 +++-- tests/system/Context/ContextTest.php | 490 ++++++++++++++++++++-- user_guide_src/source/general/logging.rst | 2 +- 4 files changed, 511 insertions(+), 84 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index 62648ee97b62..779790747611 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -872,10 +872,8 @@ public static function typography(bool $getShared = true) /** * The Context class provides a way to store and retrieve static data throughout requests. - * - * @return Context */ - public static function context(bool $getShared = true) + public static function context(bool $getShared = true): Context { if ($getShared) { return static::getSharedInstance('context'); diff --git a/system/Context/Context.php b/system/Context/Context.php index 2cd29957c8a5..4a62d2f28472 100644 --- a/system/Context/Context.php +++ b/system/Context/Context.php @@ -14,26 +14,25 @@ namespace CodeIgniter\Context; use CodeIgniter\Helpers\Array\ArrayHelper; +use SensitiveParameter; +use SensitiveParameterValue; -class Context +final class Context { /** * The data stored in the context. * * @var array */ - protected array $data; + private array $data; /** - * The data that is stored, but not included in logs. + * The data that is stored but not included in logs. * * @var array */ private array $hiddenData; - /** - * Constructor - */ public function __construct() { $this->data = []; @@ -42,6 +41,7 @@ public function __construct() /** * Set a key-value pair to the context. + * Supports dot notation for nested arrays. * * @param array|string $key The key to identify the data. Can be a string or an array of key-value pairs. * @param mixed $value The value to be stored in the context. @@ -51,43 +51,48 @@ public function __construct() public function set(array|string $key, mixed $value = null): self { if (is_array($key)) { - $this->data = array_merge($this->data, $key); + foreach ($key as $k => $v) { + ArrayHelper::dotSet($this->data, $k, $v); + } return $this; } - $this->data[$key] = $value; + ArrayHelper::dotSet($this->data, $key, $value); return $this; } /** * Set a hidden key-value pair to the context. This data will not be included in logs. + * Supports dot notation for nested arrays. * * @param array|string $key The key to identify the data. Can be a string or an array of key-value pairs. * @param mixed $value The value to be stored in the context. * * @return $this */ - public function setHidden(array|string $key, mixed $value = null): self + public function setHidden(#[SensitiveParameter] array|string $key, #[SensitiveParameter] mixed $value = null): self { if (is_array($key)) { - $this->hiddenData = array_merge($this->hiddenData, $key); + foreach ($key as $k => $v) { + ArrayHelper::dotSet($this->hiddenData, $k, $v); + } return $this; } - $this->hiddenData[$key] = $value; + ArrayHelper::dotSet($this->hiddenData, $key, $value); return $this; } /** * Get a value from the context by its key, or return a default value if the key does not exist. - * Supports dot notation for nested arrays (e.g., 'user.profile.name' to access $data['user']['profile']['name']). + * Supports dot notation for nested arrays. * - * @param string $key The key to identify the data. - * @param mixed|null $default The default value to return if the key does not exist in the context. + * @param string $key The key to identify the data. + * @param mixed $default The default value to return if the key does not exist in the context. * * @return mixed The value associated with the key, or the default value if the key does not exist. */ @@ -98,6 +103,7 @@ public function get(string $key, mixed $default = null): mixed /** * Get only the specified keys from the context. If a key does not exist, it will be ignored. + * Supports dot notation for nested arrays. * * @param list|string $keys An array of keys to retrieve from the context. * @@ -105,15 +111,12 @@ public function get(string $key, mixed $default = null): mixed */ public function getOnly(array|string $keys): array { - if (is_string($keys)) { - $keys = [$keys]; - } - - return array_filter($this->data, static fn ($k): bool => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return ArrayHelper::dotOnly($this->data, $keys); } /** * Get all keys from the context except the specified keys. + * Supports dot notation for nested arrays. * * @param list|string $keys An array of keys to exclude from the context. * @@ -121,11 +124,7 @@ public function getOnly(array|string $keys): array */ public function getExcept(array|string $keys): array { - if (is_string($keys)) { - $keys = [$keys]; - } - - return array_filter($this->data, static fn ($k): bool => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return ArrayHelper::dotExcept($this->data, $keys); } /** @@ -140,47 +139,42 @@ public function getAll(): array /** * Get a hidden value from the context by its key, or return a default value if the key does not exist. + * Supports dot notation for nested arrays. * * @param string $key The key to identify the data. * @param mixed|null $default The default value to return if the key does not exist in the context. * * @return mixed The value associated with the key, or the default value if the key does not exist. */ - public function getHidden(string $key, mixed $default = null): mixed + public function getHidden(#[SensitiveParameter] string $key, #[SensitiveParameter] mixed $default = null): mixed { return ArrayHelper::dotSearch($key, $this->hiddenData) ?? $default; } /** * Get only the specified keys from the hidden context. If a key does not exist, it will be ignored. + * Supports dot notation for nested arrays. * * @param list|string $keys An array of keys to retrieve from the hidden context. * * @return array An array of key-value pairs for the specified keys that exist in the hidden context. */ - public function getOnlyHidden(array|string $keys): array + public function getOnlyHidden(#[SensitiveParameter] array|string $keys): array { - if (is_string($keys)) { - $keys = [$keys]; - } - - return array_filter($this->hiddenData, static fn ($k): bool => in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return ArrayHelper::dotOnly($this->hiddenData, $keys); } /** * Get all keys from the hidden context except the specified keys. + * Supports dot notation for nested arrays. * * @param list|string $keys An array of keys to exclude from the hidden context. * * @return array An array of key-value pairs for all keys in the hidden context except the specified keys. */ - public function getExceptHidden(array|string $keys): array + public function getExceptHidden(#[SensitiveParameter] array|string $keys): array { - if (is_string($keys)) { - $keys = [$keys]; - } - - return array_filter($this->hiddenData, static fn ($k): bool => ! in_array($k, $keys, true), ARRAY_FILTER_USE_KEY); + return ArrayHelper::dotExcept($this->hiddenData, $keys); } /** @@ -195,6 +189,7 @@ public function getAllHidden(): array /** * Check if a key exists in the context. + * Supports dot notation for nested arrays. * * @param string $key The key to check for existence in the context. * @@ -202,11 +197,12 @@ public function getAllHidden(): array */ public function has(string $key): bool { - return ArrayHelper::dotKeyExists($key, $this->data); + return ArrayHelper::dotHas($key, $this->data); } /** * Check if a key exists in the hidden context. + * Supports dot notation for nested arrays. * * @param string $key The key to check for existence in the hidden context. * @@ -214,11 +210,12 @@ public function has(string $key): bool */ public function hasHidden(string $key): bool { - return ArrayHelper::dotKeyExists($key, $this->hiddenData); + return ArrayHelper::dotHas($key, $this->hiddenData); } /** * Remove a key-value pair from the context by its key. + * Supports dot notation for nested arrays. * * @param list|string $key The key to identify the data to be removed from the context. * @@ -228,35 +225,36 @@ public function remove(array|string $key): self { if (is_array($key)) { foreach ($key as $k) { - unset($this->data[$k]); + ArrayHelper::dotUnset($this->data, $k); } return $this; } - unset($this->data[$key]); + ArrayHelper::dotUnset($this->data, $key); return $this; } /** * Remove a key-value pair from the hidden context by its key. + * Supports dot notation for nested arrays. * * @param list|string $key The key to identify the data to be removed from the hidden context. * * @return $this */ - public function removeHidden(array|string $key): self + public function removeHidden(#[SensitiveParameter] array|string $key): self { if (is_array($key)) { foreach ($key as $k) { - unset($this->hiddenData[$k]); + ArrayHelper::dotUnset($this->hiddenData, $k); } return $this; } - unset($this->hiddenData[$key]); + ArrayHelper::dotUnset($this->hiddenData, $key); return $this; } @@ -297,4 +295,17 @@ public function clearHidden(): self return $this; } + + public function __debugInfo(): array + { + return [ + 'data' => $this->data, + 'hiddenData' => new SensitiveParameterValue($this->hiddenData), + ]; + } + + public function __clone() + { + $this->hiddenData = []; + } } diff --git a/tests/system/Context/ContextTest.php b/tests/system/Context/ContextTest.php index dacb6615fbfd..f0d3abf4893f 100644 --- a/tests/system/Context/ContextTest.php +++ b/tests/system/Context/ContextTest.php @@ -24,23 +24,32 @@ final class ContextTest extends CIUnitTestCase { public function testInitialState(): void { - $context = service('context'); + $context = single_service('context'); $this->assertSame([], $context->getAll()); $this->assertSame([], $context->getAllHidden()); } public function testSetAndGetSingleValue(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $this->assertSame(123, $context->get('user_id')); $this->assertNull($context->getHidden('user_id')); // Normal value should not be retrievable with getHidden() } + public function testSetAndGetSingleValueWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.id', 123); + + $this->assertSame(123, $context->get('user.id')); + $this->assertNull($context->getHidden('user.id')); // Normal value should not be retrievable with getHidden() + } + public function testSetAndGetMultipleValues(): void { - $context = service('context'); + $context = single_service('context'); $context->set([ 'user_id' => 123, 'username' => 'john_doe', @@ -52,18 +61,40 @@ public function testSetAndGetMultipleValues(): void $this->assertNull($context->getHidden('username')); } + public function testSetAndGetMultipleValueWithDotNotation(): void + { + $context = single_service('context'); + $context->set([ + 'user.profile.name' => 'John Doe', + 'user.profile.email' => 'john@example.com', + ]); + + $this->assertSame('John Doe', $context->get('user.profile.name')); + $this->assertSame('john@example.com', $context->get('user.profile.email')); + $this->assertNull($context->getHidden('user.profile.name')); // Normal value should not be retrievable with getHidden() + } + public function testSetAndGetSingleHiddenValue(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden('api_key', 'secret'); $this->assertSame('secret', $context->getHidden('api_key')); $this->assertNull($context->get('api_key')); // Hidden value should not be retrievable with get() } + public function testSetAndGetSingleHiddenValueWithDotNotation(): void + { + $context = single_service('context'); + $context->setHidden('api.credentials.key', 'secret'); + + $this->assertSame('secret', $context->getHidden('api.credentials.key')); + $this->assertNull($context->get('api.credentials.key')); // Hidden value should not be retrievable with get() + } + public function testSetAndGetMultipleHiddenValues(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden([ 'api_key' => 'secret', 'token' => 'abc123', @@ -75,9 +106,23 @@ public function testSetAndGetMultipleHiddenValues(): void $this->assertNull($context->get('token')); } + public function testSetAndGetMultipleHiddenValuesWithDotNotation(): void + { + $context = single_service('context'); + $context->setHidden([ + 'api.credentials.key' => 'secret', + 'api.credentials.token' => 'abc123', + ]); + + $this->assertSame('secret', $context->getHidden('api.credentials.key')); + $this->assertSame('abc123', $context->getHidden('api.credentials.token')); + $this->assertNull($context->get('api.credentials.key')); + $this->assertNull($context->get('api.credentials.token')); + } + public function testClear(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->set('username', 'john_doe'); @@ -87,9 +132,21 @@ public function testClear(): void $this->assertNull($context->get('username')); } + public function testClearWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->set('user.profile.email', 'john@example.com'); + + $context->clear(); + + $this->assertNull($context->get('user.profile.name')); + $this->assertNull($context->get('user.profile.email')); + } + public function testClearDoesntAffectHidden(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('api_key', 'secret123'); @@ -99,9 +156,21 @@ public function testClearDoesntAffectHidden(): void $this->assertSame('secret123', $context->getHidden('api_key')); // Hidden value should still be retrievable after clear() } + public function testClearWithDotNotationDoesntAffectHidden(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->setHidden('api.credentials.key', 'secret123'); + + $context->clear(); + + $this->assertNull($context->get('user.profile.name')); + $this->assertSame('secret123', $context->getHidden('api.credentials.key')); // Hidden value should still be retrievable after clear() + } + public function testClearHidden(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden('api_key', 'abcdef'); $context->setHidden('token', 'abc123'); @@ -111,9 +180,21 @@ public function testClearHidden(): void $this->assertNull($context->getHidden('token')); } + public function testClearHiddenWithDotNotation(): void + { + $context = single_service('context'); + $context->setHidden('api.credentials.key', 'abcdef'); + $context->setHidden('api.credentials.token', 'abc123'); + + $context->clearHidden(); + + $this->assertNull($context->getHidden('api.credentials.key')); + $this->assertNull($context->getHidden('api.credentials.token')); + } + public function testClearHiddenDoesntAffectNormalValues(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('api_key', 'secret123'); @@ -123,9 +204,21 @@ public function testClearHiddenDoesntAffectNormalValues(): void $this->assertNull($context->getHidden('api_key')); // Hidden value should be cleared } + public function testClearHiddenWithDotNotationDoesntAffectNormalValues(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->setHidden('api.credentials.key', 'secret123'); + + $context->clearHidden(); + + $this->assertSame('John Doe', $context->get('user.profile.name')); // Normal value should still be retrievable after clearHidden() + $this->assertNull($context->getHidden('api.credentials.key')); // Hidden value should be cleared + } + public function testClearAll(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('api_key', 'secret'); @@ -135,9 +228,21 @@ public function testClearAll(): void $this->assertNull($context->getHidden('api_key')); } + public function testClearAllWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->setHidden('api.credentials.key', 'secret'); + + $context->clearAll(); + + $this->assertNull($context->get('user.profile.name')); + $this->assertNull($context->getHidden('api.credentials.key')); + } + public function testGetWithDefaultValue(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); @@ -145,9 +250,19 @@ public function testGetWithDefaultValue(): void $this->assertSame('default', $context->get('non_existent_key', 'default')); } + public function testGetWithDotNotationAndDefaultValue(): void + { + $context = single_service('context'); + + $context->set('user.profile.name', 'John Doe'); + + $this->assertSame('John Doe', $context->get('user.profile.name', 'default')); // Existing key should return its value, not the default + $this->assertSame('default', $context->get('user.profile.non_existent_key', 'default')); + } + public function testGetOnlySingleKey(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->set('username', 'john_doe'); $context->setHidden('api_key', 'secret'); @@ -157,9 +272,25 @@ public function testGetOnlySingleKey(): void $this->assertSame([], $context->getOnly('non_existent_key')); } + public function testGetOnlySingleKeyWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->setHidden('api.credentials.key', 'secret'); + + $this->assertSame([ + 'user' => [ + 'profile' => [ + 'name' => 'John Doe', + ], + ], + ], $context->getOnly('user.profile.name')); + $this->assertSame([], $context->getOnly('user.profile.non_existent_key')); + } + public function testGetOnlyMultipleKeys(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->set('username', 'john_doe'); $context->setHidden('api_key', 'secret'); @@ -171,9 +302,28 @@ public function testGetOnlyMultipleKeys(): void $this->assertSame($expected, $context->getOnly(['user_id', 'username', 'non_existent_key'])); // non_existent_key should be ignored } + public function testGetOnlyMultipleKeysWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->set('user.profile.email', 'john.doe@example.com'); + $context->setHidden('api.credentials.key', 'secret'); + + $expected = [ + 'user' => [ + 'profile' => [ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', + ], + ], + ]; + + $this->assertSame($expected, $context->getOnly(['user.profile.name', 'user.profile.email', 'user.profile.non_existent_key'])); // non_existent_key should be ignored + } + public function testGetExceptSingleKey(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->set('username', 'john_doe'); $context->setHidden('api_key', 'secret'); @@ -184,9 +334,26 @@ public function testGetExceptSingleKey(): void $this->assertSame($expected, $context->getExcept('user_id')); // user_id should be excluded } + public function testGetExceptSingleKeyWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->set('user.profile.email', 'john.doe@example.com'); + $context->setHidden('api.credentials.key', 'secret'); + + $expected = [ + 'user' => [ + 'profile' => [ + 'email' => 'john.doe@example.com', + ], + ], + ]; + $this->assertSame($expected, $context->getExcept('user.profile.name')); // user.profile.name should be excluded + } + public function testGetExceptMultipleKeys(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->set('username', 'john_doe'); $context->setHidden('api_key', 'secret'); @@ -197,9 +364,26 @@ public function testGetExceptMultipleKeys(): void $this->assertSame($expected, $context->getExcept(['user_id', 'non_existent_key'])); // user_id should be excluded, non_existent_key should be ignored } + public function testGetExceptMultipleKeysWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->set('user.profile.email', 'john.doe@example.com'); + $context->setHidden('api.credentials.key', 'secret'); + + $expected = [ + 'user' => [ + 'profile' => [ + 'email' => 'john.doe@example.com', + ], + ], + ]; + $this->assertSame($expected, $context->getExcept(['user.profile.name', 'non_existent_key'])); // user.profile.name should be excluded, non_existent_key should be ignored + } + public function testGetAll(): void { - $context = service('context'); + $context = single_service('context'); $context->set([ 'user_id' => 123, 'username' => 'john_doe', @@ -213,9 +397,31 @@ public function testGetAll(): void $this->assertSame($expected, $context->getAll()); } + public function testGetAllWithDotNotation(): void + { + $context = single_service('context'); + $context->set([ + 'user.profile.name' => 'John Doe', + 'request.corr_id' => 'abc123', + ]); + + $expected = [ + 'user' => [ + 'profile' => [ + 'name' => 'John Doe', + ], + ], + 'request' => [ + 'corr_id' => 'abc123', + ], + ]; + + $this->assertSame($expected, $context->getAll()); + } + public function testGetHiddenWithDefaultValue(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden('some_secret_token', '123456abcdefghij'); @@ -223,9 +429,19 @@ public function testGetHiddenWithDefaultValue(): void $this->assertSame('foo', $context->getHidden('api_key', 'foo')); } + public function testGetHiddenWithDotNotationAndDefaultValue(): void + { + $context = single_service('context'); + + $context->setHidden('api.credentials.key', 'secret12345'); + + $this->assertSame('secret12345', $context->getHidden('api.credentials.key', 'default')); // Existing key should return its value, not the default + $this->assertSame('default', $context->getHidden('api.credentials.non_existent_key', 'default')); + } + public function testGetOnlyHiddenSingleKey(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('api_key', 'some_secret_api_key_here'); @@ -233,9 +449,19 @@ public function testGetOnlyHiddenSingleKey(): void $this->assertSame([], $context->getOnlyHidden('some_token')); } + public function testGetOnlyHiddenSingleKeyWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user_id', 123); + $context->setHidden('api.credentials.key', 'some_secret_api_key_here'); + + $this->assertSame(['api' => ['credentials' => ['key' => 'some_secret_api_key_here']]], $context->getOnlyHidden('api.credentials.key')); + $this->assertSame([], $context->getOnlyHidden('api.credentials.non_existent_key')); + } + public function testGetOnlyHiddenMultipleKeys(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('api_key', 'secret'); $context->setHidden('token', 'abc123'); @@ -247,9 +473,27 @@ public function testGetOnlyHiddenMultipleKeys(): void $this->assertSame($expected, $context->getOnlyHidden(['api_key', 'token', 'non_existent_key'])); // non_existent_key should be ignored } + public function testGetOnlyHiddenMultipleKeysWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user_id', 123); + $context->setHidden('api.credentials.key', 'secret'); + $context->setHidden('api.credentials.token', 'abc123'); + + $expected = [ + 'api' => [ + 'credentials' => [ + 'key' => 'secret', + 'token' => 'abc123', + ], + ], + ]; + $this->assertSame($expected, $context->getOnlyHidden(['api.credentials.key', 'api.credentials.token', 'api.credentials.non_existent_key'])); // non_existent_key should be ignored + } + public function testGetExceptHiddenSingleKey(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('some_sensitive_user_info', 'abcdefghij'); $context->setHidden('api_key', 'some_secret_api_key_here'); @@ -261,9 +505,27 @@ public function testGetExceptHiddenSingleKey(): void $this->assertSame($expected, $context->getExceptHidden('api_key')); } + public function testGetExceptHiddenSingleKeyWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user_id', 123); + $context->setHidden('api.credentials.key', 'secret'); + $context->setHidden('api.credentials.token', 'abc123'); + + $expected = [ + 'api' => [ + 'credentials' => [ + 'key' => 'secret', + ], + ], + ]; + + $this->assertSame($expected, $context->getExceptHidden('api.credentials.token')); + } + public function testGetExceptHiddenMultipleKeys(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('token', 'abc123'); $context->setHidden('api_key', 'secret'); @@ -274,9 +536,28 @@ public function testGetExceptHiddenMultipleKeys(): void $this->assertSame($expected, $context->getExceptHidden(['api_key', 'non_existent_key'])); // token should be excluded, non_existent_key should be ignored } + public function testGetExceptHiddenMultipleKeysWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user_id', 123); + $context->setHidden('api.credentials.key', 'secret'); + $context->setHidden('api.credentials.token', 'abc123'); + $context->setHidden('api.credentials.session_id', 'xyz789'); + + $expected = [ + 'api' => [ + 'credentials' => [ + 'key' => 'secret', + 'session_id' => 'xyz789', + ], + ], + ]; + $this->assertSame($expected, $context->getExceptHidden(['api.credentials.token', 'non_existent_key'])); // api.credentials.token should be excluded, non_existent_key should be ignored + } + public function testGetAllHidden(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden([ 'api_key' => 'secret', 'token' => 'abc123', @@ -290,27 +571,65 @@ public function testGetAllHidden(): void $this->assertSame($expected, $context->getAllHidden()); } + public function testGetAllHiddenWithDotNotation(): void + { + $context = single_service('context'); + $context->setHidden([ + 'api.credentials.key' => 'secret', + 'api.credentials.token' => 'abc123', + ]); + + $expected = [ + 'api' => [ + 'credentials' => [ + 'key' => 'secret', + 'token' => 'abc123', + ], + ], + ]; + + $this->assertSame($expected, $context->getAllHidden()); + } + public function testOverwriteExistingValue(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->set('user_id', 456); // Overwrite existing value $this->assertSame(456, $context->get('user_id')); } + public function testOverwriteExistingValueWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->set('user.profile.name', 'Something Different'); // Overwrite existing value + + $this->assertSame('Something Different', $context->get('user.profile.name')); + } + public function testOverwriteExistingHiddenValue(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden('api_key', 'secret'); $context->setHidden('api_key', 'new_secret'); // Overwrite existing hidden value $this->assertSame('new_secret', $context->getHidden('api_key')); } + public function testOverwriteExistingHiddenValueWithDotNotation(): void + { + $context = single_service('context'); + $context->setHidden('api.credentials.key', 'secret'); + $context->setHidden('api.credentials.key', 'new_secret'); // Overwrite existing hidden value + + $this->assertSame('new_secret', $context->getHidden('api.credentials.key')); + } + public function testSetHiddenDoesNotAffectNormalValues(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->setHidden('user_id', 'hidden_value'); @@ -318,9 +637,19 @@ public function testSetHiddenDoesNotAffectNormalValues(): void $this->assertSame('hidden_value', $context->getHidden('user_id')); // Hidden value should be retrievable with getHidden() } + public function testSetHiddenWithDotNotationDoesNotAffectNormalValues(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->setHidden('user.profile.name', 'Hidden Name'); + + $this->assertSame('John Doe', $context->get('user.profile.name')); // Normal value should still be retrievable + $this->assertSame('Hidden Name', $context->getHidden('user.profile.name')); // Hidden value should be retrievable with getHidden() + } + public function testHasKey(): void { - $context = service('context'); + $context = single_service('context'); $this->assertFalse($context->has('user_id')); $context->set('user_id', 123); @@ -328,18 +657,37 @@ public function testHasKey(): void $this->assertTrue($context->has('user_id')); } + public function testHasKeyWithDotNotation(): void + { + $context = single_service('context'); + $this->assertFalse($context->has('user.profile.name')); + + $context->set('user.profile.name', 'John Doe'); + + $this->assertTrue($context->has('user.profile.name')); + } + public function testHasHiddenKey(): void { - $context = service('context'); + $context = single_service('context'); $this->assertFalse($context->hasHidden('api_key')); $context->setHidden('api_key', 'secret'); $this->assertTrue($context->hasHidden('api_key')); } + public function testHasHiddenKeyWithDotNotation(): void + { + $context = single_service('context'); + $this->assertFalse($context->hasHidden('api.credentials.key')); + + $context->setHidden('api.credentials.key', 'secret'); + $this->assertTrue($context->hasHidden('api.credentials.key')); + } + public function testRemoveSingleValue(): void { - $context = service('context'); + $context = single_service('context'); $context->set('user_id', 123); $context->set('username', 'john_doe'); $context->remove('user_id'); @@ -348,9 +696,20 @@ public function testRemoveSingleValue(): void $this->assertSame('john_doe', $context->get('username')); // Ensure other values are unaffected } + public function testRemoveSingleValueWithDotNotation(): void + { + $context = single_service('context'); + $context->set('user.profile.name', 'John Doe'); + $context->set('user.profile.email', 'john@example.com'); + $context->remove('user.profile.name'); + + $this->assertNull($context->get('user.profile.name')); + $this->assertSame('john@example.com', $context->get('user.profile.email')); // Ensure other values are unaffected + } + public function testRemoveMultipleValues(): void { - $context = service('context'); + $context = single_service('context'); $context->set([ 'user_id' => 123, 'username' => 'john_doe', @@ -364,9 +723,25 @@ public function testRemoveMultipleValues(): void $this->assertSame('john@example.com', $context->get('email')); // Ensure other values are unaffected } + public function testRemoveMultipleValuesWithDotNotation(): void + { + $context = single_service('context'); + $context->set([ + 'user.id' => 123, + 'request.corr_id' => '12345', + 'user.email' => 'john@example.com', + ]); + + $context->remove(['user.id', 'request.corr_id']); + + $this->assertNull($context->get('user.id')); + $this->assertNull($context->get('request.corr_id')); + $this->assertSame('john@example.com', $context->get('user.email')); + } + public function testRemoveHiddenValue(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden('api_key', 'secret'); $context->setHidden('token', 'abc123'); @@ -375,9 +750,20 @@ public function testRemoveHiddenValue(): void $this->assertSame('abc123', $context->getHidden('token')); // Ensure other hidden values are unaffected } + public function testRemoveHiddenValueWithDotNotation(): void + { + $context = single_service('context'); + $context->setHidden('credentials.api_key', 'secret'); + $context->setHidden('credentials.token', 'abc123'); + + $context->removeHidden('credentials.api_key'); + $this->assertNull($context->getHidden('credentials.api_key')); + $this->assertSame('abc123', $context->getHidden('credentials.token')); // Ensure other hidden values are unaffected + } + public function testRemoveMultipleHiddenValues(): void { - $context = service('context'); + $context = single_service('context'); $context->setHidden([ 'api_key' => 'secret', 'token' => 'abc123', @@ -391,12 +777,44 @@ public function testRemoveMultipleHiddenValues(): void $this->assertSame('xyz789', $context->getHidden('session_id')); // Ensure other hidden values are unaffected } - protected function tearDown(): void + public function testRemoveMultipleHiddenValuesWithDotNotation(): void { - parent::tearDown(); + $context = single_service('context'); + $context->setHidden([ + 'credentials.api_key' => 'secret', + 'credentials.token' => 'abc123', + 'session_id' => 'xyz789', + ]); - // Clear the context after each test to ensure isolation. - $context = service('context'); - $context->clearAll(); + $context->removeHidden(['credentials.api_key', 'credentials.token']); + + $this->assertNull($context->getHidden('credentials.api_key')); + $this->assertNull($context->getHidden('credentials.token')); + $this->assertSame('xyz789', $context->getHidden('session_id')); // Ensure other hidden values are unaffected + } + + public function testPrintRDoesNotExposeHiddenValues(): void + { + $context = new Context(); + $context->set('user_id', 123); + $context->setHidden('credentials.api_key', 'secret'); + + $output = print_r($context, true); + + $this->assertStringContainsString('user_id', $output); + $this->assertStringNotContainsString('secret', $output); + $this->assertStringContainsString('SensitiveParameterValue', $output); + } + + public function testCloneDoesNotCopyHiddenValues(): void + { + $context = new Context(); + $context->set('user_id', 123); + $context->setHidden('credentials.api_key', 'secret'); + + $clonedContext = clone $context; + + $this->assertSame(123, $clonedContext->get('user_id')); // Normal value should be copied + $this->assertNull($clonedContext->getHidden('credentials.api_key')); // Hidden value should not be copied } } diff --git a/user_guide_src/source/general/logging.rst b/user_guide_src/source/general/logging.rst index 1c2fc453eb64..dd3c6da533b3 100644 --- a/user_guide_src/source/general/logging.rst +++ b/user_guide_src/source/general/logging.rst @@ -141,7 +141,7 @@ This would produce a log entry like: ERROR - 2026-02-18 --> Payment processing failed {"user_id":123,"transaction_id":"txn_12345"} -.. note:: Hidden data set with ``setHidden()`` is **never** included in log output, even when +.. note:: Hidden data set with ``setHidden()`` are **never** included in log output, even when ``$logGlobalContext`` is enabled. This protects sensitive information such as API keys and tokens from appearing in log files. From 4bd55ed4c2f710963df977d3dcbca3af460d464a Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 24 Feb 2026 16:41:31 +0530 Subject: [PATCH 18/19] changed type from mixed|null to mixed --- system/Context/Context.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Context/Context.php b/system/Context/Context.php index 4a62d2f28472..05b9abb48f6e 100644 --- a/system/Context/Context.php +++ b/system/Context/Context.php @@ -142,7 +142,7 @@ public function getAll(): array * Supports dot notation for nested arrays. * * @param string $key The key to identify the data. - * @param mixed|null $default The default value to return if the key does not exist in the context. + * @param mixed $default The default value to return if the key does not exist in the context. * * @return mixed The value associated with the key, or the default value if the key does not exist. */ From f0664880ec82b0c947867184c96ff246ab0dd88a Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 24 Feb 2026 16:49:12 +0530 Subject: [PATCH 19/19] Added __serialize and __unserialize methods. --- system/Context/Context.php | 20 ++++++++++++++++++-- tests/system/Context/ContextTest.php | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/system/Context/Context.php b/system/Context/Context.php index 05b9abb48f6e..f0f3f1a2fd50 100644 --- a/system/Context/Context.php +++ b/system/Context/Context.php @@ -141,8 +141,8 @@ public function getAll(): array * Get a hidden value from the context by its key, or return a default value if the key does not exist. * Supports dot notation for nested arrays. * - * @param string $key The key to identify the data. - * @param mixed $default The default value to return if the key does not exist in the context. + * @param string $key The key to identify the data. + * @param mixed $default The default value to return if the key does not exist in the context. * * @return mixed The value associated with the key, or the default value if the key does not exist. */ @@ -308,4 +308,20 @@ public function __clone() { $this->hiddenData = []; } + + public function __serialize(): array + { + return [ + 'data' => $this->data, + ]; + } + + /** + * @param array $data + */ + public function __unserialize(array $data): void + { + $this->data = $data['data'] ?? []; + $this->hiddenData = []; + } } diff --git a/tests/system/Context/ContextTest.php b/tests/system/Context/ContextTest.php index f0d3abf4893f..daef7cdb312d 100644 --- a/tests/system/Context/ContextTest.php +++ b/tests/system/Context/ContextTest.php @@ -817,4 +817,21 @@ public function testCloneDoesNotCopyHiddenValues(): void $this->assertSame(123, $clonedContext->get('user_id')); // Normal value should be copied $this->assertNull($clonedContext->getHidden('credentials.api_key')); // Hidden value should not be copied } + + public function testSerializationDoesNotIncludeHiddenValues(): void + { + $context = new Context(); + $context->set('user_id', 123); + $context->setHidden('credentials.api_key', 'secret'); + + $serialized = serialize($context); + + $this->assertStringContainsString('user_id', $serialized); + $this->assertStringNotContainsString('secret', $serialized); + + $unserializedContext = unserialize($serialized); + + $this->assertSame(123, $unserializedContext->get('user_id')); // Normal value should be preserved + $this->assertNull($unserializedContext->getHidden('credentials.api_key')); // Hidden value should not be preserved + } }