Skip to content

Comments

feat: Add Global Context feature#9970

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

feat: Add Global Context feature#9970
patel-vansh wants to merge 17 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.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I guess adding the methods for modifying and removing dot notation in arrays in ArrayHelper would be a great feature to have in 4.8. and since if those methods are getting added, then having that thing here would be a addon instead of overkill.

So, my vote goes to adding dot notation in Context.

@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.

Comment on lines +103 to +110
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);
}
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.

Comment on lines +119 to +126
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);
}
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.

@datamweb
Copy link
Contributor

We reset pretty much everything - one way or another.

@michalsn A major part of my security concerns is now resolved. Thanks!

@patel-vansh
Copy link
Contributor Author

Hello all. I was looking at the discussion and I think, we can also do one more thing for this feature.

What if we introduce a third array $shared??
Like the context service will become persistent service. The shared data will be kept and will be available across multiple requests.

The normal as well as hidden data will be resetted after every request. (Though I haven't seen much implementation of Worker mode and I don't know much how services are cleared and reinitialised) This is just a thought.

This shared array will only benefit when worker mode is enabled. Else everything will be normal.

This is just a thought.
Everyone's thoughts are welcome.

@michalsn
Copy link
Member

@patel-vansh Technically, this would be doable if a dedicated method were provided. However, I'm not sure about the practical benefits - why would we need this? I don't recall other frameworks offering this feature.

@patel-vansh
Copy link
Contributor Author

@michalsn Yeah, I think we can leave $shared array for now. If we need that in future, we can always add that.

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