Skip to content

Comments

feat: Add Global Context feature#9970

Open
patel-vansh wants to merge 12 commits intocodeigniter4:4.8from
patel-vansh:feat/context
Open

feat: Add Global Context feature#9970
patel-vansh wants to merge 12 commits intocodeigniter4:4.8from
patel-vansh:feat/context

Conversation

@patel-vansh
Copy link
Contributor

Description
This PR adds Global Context feature for requests.

Normal context is automatically added to logs if $logGlobalContext is enabled in app/Config/Logger.php file. This provides better monitoring for any logs or exceptions along with maintaining security with hidden context feature.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@paulbalandan paulbalandan added 4.8 PRs that target the `4.8` branch. new feature PRs for new features labels Feb 21, 2026
@michalsn
Copy link
Member

Before deciding whether we need this, we should ask if other frameworks have already faced it. I did some research.

Laravel introduced a first-class Context facade in v11. The idea is simple: set data once in middleware and have it automatically available everywhere (including in log entries) without passing values through every method. It even propagates context into queued jobs, so a request_id is passed to a queue worker.

Symfony approaches it differently. It uses Request::attributes to carry things like tenant or user, resolved early in the request lifecycle. That works well inside HTTP, but outside it, in console commands, and in queues, it's not available.

What this PR is proposing is closer to Laravel model: a single, consistent concept that works the same whether we're handling HTTP, or running a command.

Why is context useful? When something breaks in production, we want every log entry from that request to have the same request_id or user_id, not just the line where the exception happened. Another example: not every app uses sessions. A stateless API authenticated by API key still needs a way to resolve identity once and share it throughout the call stack. Context fills that gap naturally.

@patel-vansh
Copy link
Contributor Author

patel-vansh commented Feb 21, 2026

@michalsn Yes. When I myself created few pure API projects with CI 4, I almost everytime have to create custom solution for global context management. Also for better traceability and monitoring, central global context is a good idea (I think). With just one boolean toggle to turn on or off the logging of this context will enable devs to switch between dev environment (where all log levels are enabled and log files fill up really quickly) and prod environment (where not all log levels are enabled but you need traceability) really easily without much headache.

For normal website projects, this is not much useful as you have sessions and cookies.

But I guess this feature would serve as helpful addon rather than unnecessary debt. But again, decision makers are community members and maintainers, not me.

@neznaika0
Copy link
Contributor

Haven't worked with this. The attributes in the symfony contain a twig.

@michalsn
Copy link
Member

Haven't worked with this. The attributes in the symfony contain a twig.

Do you mean that Symfony request attributes are accessible from templates directly?

Copy link
Member

@michalsn michalsn left a comment

Choose a reason for hiding this comment

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

Overall, I think the new Context service is a valuable addition and the implementation is heading in a good direction.

I have one suggestion: context data should be displayed in the Toolbar under the "Vars" section, with a clear separation between normal and hidden data. But this may also be done in a separate PR.

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

Choose a reason for hiding this comment

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

This probably should be true by default.

Comment on lines +91 to +94
public function get(string $key, mixed $default = null): mixed
{
return $this->data[$key] ?? $default;
}
Copy link
Member

Choose a reason for hiding this comment

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

Dot notation is widely used for nested data access in the framework already. Given that, it would be good to align Context behavior as well, so dotted keys are treated as paths for nested arrays rather than plain string keys.

I suggest handling this consistently across get()/has()/remove() (and their hidden variants).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For dot notation, the ArrayHelper.php has method for getting values and to check if value exists in array or not, but it doesn't have any for modifying or removing any. Would you like to create those methods in ArrayHelper first and then use those in here, or I would just declare private methods in Context.php itself for now, and then in separate PR we would just add that in ArrayHelper.php?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, for methods like getOnly and getExcept, we need to think about ArrayHelper methods for that too.

Copy link
Member

Choose a reason for hiding this comment

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

Good point. I've started implementing this in ArrayHelper and will send a PR today.

As I was working on it, I had second thoughts about whether we should add support for dot notation here, since most users will probably be working with flat arrays. Now, I'm not entirely sure this is the right approach (an overkill), but I'd be interested to hear what others think.

Comment on lines +200 to +203
public function missing(string $key): bool
{
return ! $this->has($key);
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a big fan of all the missing* methods, since we don't usually implement these in the framework. We generally prefer working directly with ! $this->has() when needed.

*
* @var bool
*/
protected $logGlobalContext;
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
protected $logGlobalContext;
protected bool $logGlobalContext = true

$this->logCache = [];
}

$this->logGlobalContext = $config->logGlobalContext;
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
$this->logGlobalContext = $config->logGlobalContext;
$this->logGlobalContext = $config->logGlobalContext ?? $this->logGlobalContext;

Comment on lines +266 to +272
if ($this->logGlobalContext) {
$globalContext = service('context')->getAll();
if (is_array($globalContext) && $globalContext !== []) {
$message .= ' ' . json_encode($globalContext);
}
}

Copy link
Member

Choose a reason for hiding this comment

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

The check is_array($globalContext) is not needed here as this variable is guaranteed to be an array.

Appending global context JSON directly to the message is acceptable for file-based output. However, for non-file handlers (e.g. external log systems, or even ChromeLogger), this is not ideal because context becomes part of the message string rather than structured metadata.

I think we should merge this PR with the current logger behavior, then follow up with a separate PR to redesign logger/handler integration so handlers can receive structured global context cleanly.

I'm happy to prepare that logger follow-up PR after/if this one is merged.

@paulbalandan
Copy link
Member

@datamweb instead of downvoting, I think it would be more beneficial for all to hear your thoughts on this.

@neznaika0
Copy link
Contributor

Do you mean that Symfony request attributes are accessible from templates directly?

Request available as https://symfony.com/doc/current/templates.html#the-app-global-variable

I haven't tested a symfony in a long time. There was something like that once:
$request->setAttribute("twig", new Environment())

@paulbalandan paulbalandan deleted the branch codeigniter4:4.8 February 22, 2026 10:37
@paulbalandan paulbalandan reopened this Feb 22, 2026
@michalsn
Copy link
Member

Thanks @neznaika0. I believe this is a topic for a separate discussion, as it would involve changes to the Parser to make the context automatically available. It's something we can consider in the future, although I'm not a big fan of the idea. For now, let's focus on the Context class itself.

@datamweb
Copy link
Contributor

datamweb commented Feb 22, 2026

@paulbalandan To be completely honest, I read this entire PR yesterday. I initially held back from commenting because I truly didn't want to drain the energy of an active contributor. The core concept of a Context is highly valuable and modern, but my downvote stems from severe concerns regarding the implementation and the default behavior.

Here is a breakdown of my technical concerns:

Recently, we have been moving towards FrankenPHP, and a part of the CI4 community relies on the CodeIgniter4-Burner library (which enables high-performance servers like RoadRunner, OpenSwoole, and Workerman).

In these persistent, long-running worker environments, the application does not die after a request. If the Context class is implemented as a shared service or global state without a bulletproof, automatic reset() hook explicitly tied to the end of the HTTP lifecycle, we will encounter State Bleeding. Metadata from Request A (e.g., service('context')->set('user_id', 12)) will permanently reside in memory and bleed into Request B, Request C, and so on. Has this implementation been strictly tested against asynchronous/worker environments to guarantee memory isolation?

As developers begin adopting this new feature in 4.8.0 to store general request data (emails, IPs, phone), they naturally won't mark everything as hidden (since they aren't strict "passwords"). Because $logGlobalContext is true by default, developers will unknowingly fall into a trap where the framework silently appends sensitive PII to plain-text log files every time a log is generated across the application.

I am not against the feature itself, but I strongly advocate for the following before we merge this into the Core:

Keep $logGlobalContext = false as the default to protect users from unexpected privacy leaks and log bloat.
Ensure there are strict lifecycle hooks proving that Context automatically clears itself in RoadRunner/FrankenPHP/Swoole environments.

If you believe I am misunderstanding any part of the implementation or its guarantees, please feel free to correct me.

@michalsn
Copy link
Member

@datamweb

You are right to call out the risk of accidental context logging. I had proposed $logGlobalContext = true by default for convenience, but I agree that safer framework defaults should win here.

As for worker mode, context is not in the persistent services list, and Services::resetForWorkerMode() runs between requests, so the shared context instance is dropped per request cycle.

@datamweb
Copy link
Contributor

As for worker mode, context is not in the persistent services list, and Services::resetForWorkerMode() runs between requests, so the shared context instance is dropped per request cycle.

@michalsn Thanks for clarifying!
What happens if a developer mistakenly caches the service in a class property?

class AuthFilter implements FilterInterface
{
    public function __construct() {
        $this->context = service('context'); // Cached once per worker
    }
}

Since the AuthFilter instance stays alive across multiple requests, wouldn’t $this->context hold onto the stale object from Request A, completely bypassing Services::resetForWorkerMode()?

If so, should we add a strict warning in the docs advising developers to never store service('context') in class properties and always call it dynamically?

@michalsn
Copy link
Member

@datamweb Filters do not persist between requests. We reset pretty much everything - one way or another.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch. new feature PRs for new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants