Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/Config/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions system/Common.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions system/Config/Services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -875,4 +876,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static function context(bool $getShared = true)
public static function context(bool $getShared = true): Context

{
if ($getShared) {
return static::getSharedInstance('context');
}

return new Context();
}
}
300 changes: 300 additions & 0 deletions system/Context/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* 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;

class Context
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're handling potentially sensitive data via the $hidden property, how do we handle if the Context object is accidentally var_dump()ed, `? This should be probably be disallowed for cloning and serialization too, right?

{
/**
* The data stored in the context.
*
* @var array<string, mixed>
*/
protected array $data;

/**
* The data that is stored, but not included in logs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The data that is stored, but not included in logs.
* The data that is stored but not included in logs.

*
* @var array<string, mixed>
*/
private array $hiddenData;

/**
* Constructor
*/
Comment on lines +34 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Constructor
*/

public function __construct()
{
$this->data = [];
$this->hiddenData = [];
}

/**
* Set a key-value pair to the context.
*
* @param array<string, mixed>|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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this class meant to be extended? If yes, better to have the return as static. If no, then this can retain and perhaps make the class final?

{
if (is_array($key)) {
$this->data = array_merge($this->data, $key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use unpacking here:

Suggested change
$this->data = array_merge($this->data, $key);
$this->data = [...$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 array<string, mixed>|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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add the #[SensitiveParameter] attribute to the $value since it is sensitive. For $key too maybe since it can become an array containing the secret. Please do this to all *Hidden methods.

{
if (is_array($key)) {
$this->hiddenData = array_merge($this->hiddenData, $key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use unpacking


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.
* 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mixed already includes null

*
* @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.
*
* @param list<string>|string $keys An array of keys to retrieve from the context.
*
* @return array<string, mixed> An array of key-value pairs for the specified keys that exist in the context.
*/
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);
}
Comment on lines +106 to +113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran a local benchmark using the following setup:
Context data size: ~1,000 entries, Requested keys: 5, Iterations: 50,000
On my system, the results were:

Old Method (array_filter): 5.7311 seconds
New Method (array_intersect): 0.1905 seconds
Performance Gain: 30.1x faster

Even though a typical application is unlikely to store this many entries in the Context, I believe the framework core should be optimized for worst‑case scenarios by default.

Suggested change
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);
}
public function getOnly(array|string $keys): array
{
$keys = (array) $keys;
return array_intersect_key($this->data, array_flip($keys));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's really good performance boost. But with new suggestion of supporting dot notation, I guess this method would change completely.


/**
* Get all keys from the context except the specified keys.
*
* @param list<string>|string $keys An array of keys to exclude from the context.
*
* @return array<string, mixed> An array of key-value pairs for all keys in the context except the specified keys.
*/
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);
}
Comment on lines +122 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old Method (array_filter): 6.3526 seconds
New Method (array_diff_key): 0.689 seconds
Performance Gain: 9.2x faster

Suggested change
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);
}
public function getExcept(array|string $keys): array
{
$keys = (array) $keys;
return array_diff_key($this->data, array_flip($keys));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.


/**
* Get all data from the context
*
* @return array<string, mixed> 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 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.
*
* @param list<string>|string $keys An array of keys to retrieve from the hidden context.
*
* @return array<string, mixed> An array of key-value pairs for the specified keys that exist in the hidden context.
*/
public function getOnlyHidden(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);
}

/**
* Get all keys from the hidden context except the specified keys.
*
* @param list<string>|string $keys An array of keys to exclude from the hidden context.
*
* @return array<string, mixed> An array of key-value pairs for all keys in the hidden context except the specified keys.
*/
public function getExceptHidden(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);
}

/**
* Get all hidden data from the context
*
* @return array<string, mixed> 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 ArrayHelper::dotKeyExists($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 ArrayHelper::dotKeyExists($key, $this->hiddenData);
}

/**
* Remove a key-value pair from the context by its key.
*
* @param list<string>|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) {
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 list<string>|string $key The key to identify the data to be removed from the hidden context.
*
* @return $this
*/
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;
}

/**
* 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;
}
}
Loading
Loading