diff --git a/app/Config/Logger.php b/app/Config/Logger.php index 799dc2c39080..e303965a397b 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -51,6 +51,22 @@ 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. + */ + public bool $logGlobalContext = false; + /** * -------------------------------------------------------------------------- * Log Handlers diff --git a/system/Common.php b/system/Common.php index bcf2a5c14db9..04115993ef87 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,17 @@ 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. + */ + function context(): Context + { + return service('context'); + } +} + if (! function_exists('cookie')) { /** * Simpler way to create a new Cookie instance. diff --git a/system/Config/Services.php b/system/Config/Services.php index 3e87b6ab78bd..779790747611 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; @@ -868,4 +869,16 @@ public static function typography(bool $getShared = true) return new Typography(); } + + /** + * The Context class provides a way to store and retrieve static data throughout requests. + */ + public static function context(bool $getShared = true): Context + { + 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..f0f3f1a2fd50 --- /dev/null +++ b/system/Context/Context.php @@ -0,0 +1,327 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Context; + +use CodeIgniter\Helpers\Array\ArrayHelper; +use SensitiveParameter; +use SensitiveParameterValue; + +final class Context +{ + /** + * The data stored in the context. + * + * @var array + */ + private array $data; + + /** + * The data that is stored but not included in logs. + * + * @var array + */ + private array $hiddenData; + + public function __construct() + { + $this->data = []; + $this->hiddenData = []; + } + + /** + * 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. + * + * @return $this + */ + public function set(array|string $key, mixed $value = null): self + { + if (is_array($key)) { + foreach ($key as $k => $v) { + ArrayHelper::dotSet($this->data, $k, $v); + } + + return $this; + } + + 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(#[SensitiveParameter] array|string $key, #[SensitiveParameter] mixed $value = null): self + { + if (is_array($key)) { + foreach ($key as $k => $v) { + ArrayHelper::dotSet($this->hiddenData, $k, $v); + } + + return $this; + } + + 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. + * + * @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. + */ + public function get(string $key, mixed $default = null): mixed + { + return ArrayHelper::dotSearch($key, $this->data) ?? $default; + } + + /** + * 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. + * + * @return array An array of key-value pairs for the specified keys that exist in the context. + */ + public function getOnly(array|string $keys): array + { + 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. + * + * @return array An array of key-value pairs for all keys in the context except the specified keys. + */ + public function getExcept(array|string $keys): array + { + return ArrayHelper::dotExcept($this->data, $keys); + } + + /** + * 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. + * 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. + * + * @return mixed The value associated with the key, or the default value if the key does not exist. + */ + 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(#[SensitiveParameter] array|string $keys): array + { + 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(#[SensitiveParameter] array|string $keys): array + { + return ArrayHelper::dotExcept($this->hiddenData, $keys); + } + + /** + * 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. + * Supports dot notation for nested arrays. + * + * @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 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. + * + * @return bool True if the key exists in the hidden context, false otherwise. + */ + public function hasHidden(string $key): bool + { + 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. + * + * @return $this + */ + public function remove(array|string $key): self + { + if (is_array($key)) { + foreach ($key as $k) { + ArrayHelper::dotUnset($this->data, $k); + } + + return $this; + } + + 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(#[SensitiveParameter] array|string $key): self + { + if (is_array($key)) { + foreach ($key as $k) { + ArrayHelper::dotUnset($this->hiddenData, $k); + } + + return $this; + } + + ArrayHelper::dotUnset($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; + } + + public function __debugInfo(): array + { + return [ + 'data' => $this->data, + 'hiddenData' => new SensitiveParameterValue($this->hiddenData), + ]; + } + + 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/system/Log/Logger.php b/system/Log/Logger.php index 8308ddaf94f7..87edf8fbc14c 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -114,6 +114,13 @@ class Logger implements LoggerInterface */ protected $cacheLogs = false; + /** + * Whether to log the global context data. + * + * Set in app/Config/Logger.php + */ + protected bool $logGlobalContext = false; + /** * Constructor. * @@ -154,6 +161,8 @@ public function __construct($config, bool $debug = CI_DEBUG) if ($this->cacheLogs) { $this->logCache = []; } + + $this->logGlobalContext = $config->logGlobalContext ?? $this->logGlobalContext; } /** @@ -252,6 +261,13 @@ public function log($level, string|Stringable $message, array $context = []): vo $message = $this->interpolate($message, $context); + if ($this->logGlobalContext) { + $globalContext = service('context')->getAll(); + if ($globalContext !== []) { + $message .= ' ' . json_encode($globalContext); + } + } + if ($this->cacheLogs) { $this->logCache[] = ['level' => $level, 'msg' => $message]; } 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')); + } } diff --git a/tests/system/Context/ContextTest.php b/tests/system/Context/ContextTest.php new file mode 100644 index 000000000000..daef7cdb312d --- /dev/null +++ b/tests/system/Context/ContextTest.php @@ -0,0 +1,837 @@ + + * + * 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; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class ContextTest extends CIUnitTestCase +{ + public function testInitialState(): void + { + $context = single_service('context'); + $this->assertSame([], $context->getAll()); + $this->assertSame([], $context->getAllHidden()); + } + + public function testSetAndGetSingleValue(): 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 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 = single_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 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 = 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_service('context'); + $context->set([ + 'user_id' => 123, + 'username' => 'john_doe', + ]); + + $expected = [ + 'user_id' => 123, + 'username' => 'john_doe', + ]; + + $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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_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 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 = single_service('context'); + $context->setHidden([ + 'api_key' => 'secret', + 'token' => 'abc123', + ]); + + $expected = [ + 'api_key' => 'secret', + 'token' => 'abc123', + ]; + + $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 = 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 = 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 = single_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 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 = single_service('context'); + $this->assertFalse($context->has('user_id')); + + $context->set('user_id', 123); + + $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 = 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 = single_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 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 = single_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 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 = single_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 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 = single_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 + } + + public function testRemoveMultipleHiddenValuesWithDotNotation(): void + { + $context = single_service('context'); + $context->setHidden([ + 'credentials.api_key' => 'secret', + 'credentials.token' => 'abc123', + 'session_id' => 'xyz789', + ]); + + $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 + } + + 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 + } +} diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index 80d42d6c83b3..136dc4332bd5 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]); + } } diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index ee6b70d4fb68..a4e0beb407dd 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -148,6 +148,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 new file mode 100644 index 000000000000..5c9dac35b16b --- /dev/null +++ b/user_guide_src/source/general/context.rst @@ -0,0 +1,374 @@ +.. _context: + +################### +Context +################### + +.. versionadded:: 4.8.0 + +.. 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 Class +*********************** + +You can access the Context service anywhere in your application using the ``service()`` function or ``context()`` helper: + +.. 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 + +********************* +Removing Context Data +********************* + +Removing a Single Value +======================== + +You can remove data from the context using the ``remove()`` method: + +.. literalinclude:: context/011.php + +Removing Multiple Values +========================= + +To remove multiple keys at once, pass an array: + +.. literalinclude:: context/012.php + +Clearing All Data +================= + +To remove all context data: + +.. literalinclude:: context/013.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/014.php + +You can also set multiple hidden values at once: + +.. literalinclude:: context/015.php + +Getting Hidden Data +=================== + +Retrieve hidden data using ``getHidden()``: + +.. literalinclude:: context/016.php + +The same methods available for regular data also work with hidden data: + +.. literalinclude:: context/017.php + +Checking Hidden Data +==================== + +Check if a hidden key exists: + +.. literalinclude:: context/018.php + +Removing Hidden Data +==================== + +Remove hidden data using ``removeHidden()``: + +.. literalinclude:: context/019.php + +Clearing Hidden Data +==================== + +To clear all hidden data without affecting regular context data: + +.. literalinclude:: context/020.php + +To clear both regular and hidden data: + +.. 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. + +*********************************** +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/022.php + +When enabled, all context data (excluding hidden data) will be automatically appended to your log messages as JSON: + +.. literalinclude:: context/023.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..53004b9f25c5 --- /dev/null +++ b/user_guide_src/source/general/context/001.php @@ -0,0 +1,6 @@ +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..60d2ca87ee2f --- /dev/null +++ b/user_guide_src/source/general/context/003.php @@ -0,0 +1,8 @@ +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..29dd1e38cafc --- /dev/null +++ b/user_guide_src/source/general/context/004.php @@ -0,0 +1,5 @@ +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..1ddd78820180 --- /dev/null +++ b/user_guide_src/source/general/context/005.php @@ -0,0 +1,4 @@ +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..c56335f6bc27 --- /dev/null +++ b/user_guide_src/source/general/context/006.php @@ -0,0 +1,4 @@ +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..9ad340998671 --- /dev/null +++ b/user_guide_src/source/general/context/007.php @@ -0,0 +1,4 @@ +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..519ecf64e174 --- /dev/null +++ b/user_guide_src/source/general/context/008.php @@ -0,0 +1,8 @@ +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..2c5158dd36b0 --- /dev/null +++ b/user_guide_src/source/general/context/009.php @@ -0,0 +1,8 @@ +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..61b10e76a4c5 --- /dev/null +++ b/user_guide_src/source/general/context/010.php @@ -0,0 +1,5 @@ +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..2c6c3598ab91 --- /dev/null +++ b/user_guide_src/source/general/context/011.php @@ -0,0 +1,3 @@ +remove('user_id'); 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..5922fd6605ac --- /dev/null +++ b/user_guide_src/source/general/context/012.php @@ -0,0 +1,3 @@ +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 new file mode 100644 index 000000000000..18746e68281d --- /dev/null +++ b/user_guide_src/source/general/context/013.php @@ -0,0 +1,3 @@ +clear(); 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..560f233a600a --- /dev/null +++ b/user_guide_src/source/general/context/014.php @@ -0,0 +1,3 @@ +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 new file mode 100644 index 000000000000..b7b3a906ac78 --- /dev/null +++ b/user_guide_src/source/general/context/015.php @@ -0,0 +1,7 @@ +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 new file mode 100644 index 000000000000..e26ceda619ff --- /dev/null +++ b/user_guide_src/source/general/context/016.php @@ -0,0 +1,4 @@ +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 new file mode 100644 index 000000000000..9c52af5e6b05 --- /dev/null +++ b/user_guide_src/source/general/context/017.php @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000000..b5084a23f143 --- /dev/null +++ b/user_guide_src/source/general/context/018.php @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000000..a06a3f6d9c3b --- /dev/null +++ b/user_guide_src/source/general/context/019.php @@ -0,0 +1,7 @@ +removeHidden('api_key'); + +// 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 new file mode 100644 index 000000000000..7fca9aa5869f --- /dev/null +++ b/user_guide_src/source/general/context/020.php @@ -0,0 +1,3 @@ +clearHidden(); 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..bc26dead4e00 --- /dev/null +++ b/user_guide_src/source/general/context/021.php @@ -0,0 +1,3 @@ +clearAll(); 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..7e9f878c30bb --- /dev/null +++ b/user_guide_src/source/general/context/022.php @@ -0,0 +1,14 @@ +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 diff --git a/user_guide_src/source/general/logging.rst b/user_guide_src/source/general/logging.rst index 6e159827b1e9..dd3c6da533b3 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/023.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()`` 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. + +See :ref:`context` for full documentation on storing and managing context data. + Using Third-Party Loggers =========================