Best Practices
The Context
api is definitely a new pattern and approach to solve the context passing problem in PHP.
Whenever a new pattern is introduced, is always important to define some best practices for its good use.
These are some of what we consider to be those best practices.
Custom functions SHOULD be defined to work with Context
Context is all about composition, and you should also compose the base context functions to provide a nicer api to store or retrieve context values, like we did in the logger example. Of course, you could also rely on static methods, but in my personal opinion, functions are nicer.
You can even go as far as to create your own Context
implementation that composes a Context
. We do that for some
of our libraries. But we even discourage that. The thinner you can keep Context
api surface, the better.
Context
SHOULD be the first argument of the function or method
Whenever a function or method call accepts a Context
, this should be the first argument of the function, and
it should be named $ctx
. We know some people hate abbreviations and prefer more explicit names. I agree with that
suggestion almost in every situation, but in this case the abbreviation does not yield any sort of confusion and
is pretty universal and easily understood.
It also has the benefit that it is short, and since Context
is there to be added to your public api, we want
to minimize as much as possible the space impact that has in your method's argument list.
Enums SHOULD be used as keys rather than strings
When calling Context\withValue()
prefer enums as keys. They are lightweight, offer autocompletion, and they cannot
collide like strings could. In the case that your application still does not support PHP 8.1 and above, you MUST use
string keys with a vendor namespace.
Context
SHOULD NOT be stored inside other data structures
Always explicitly pass Context
. Do not store it inside objects or arrays, unless is explicitly necessary. For
instance, if you are using the Context
api in PSR-7 applications, it is very likely your Context
instance
will be stored in the Request attributes (which is implemented by an array). This is acceptable, but for a better
HTTP layer supporting context natively, we recommend castor/http
.
Context\withValue
SHOULD NOT be overused
Because of its particular implementation, every time you add a value to a Context
, you increase the potential call
stack size to reach the value by 1. Although the performance impact of this is negligent, is still slower than fetching
a value directly from a map, for instance.
So, bottom line, don't overuse Context\withValue
. This means that if you have to store related values in
Context
, store a data structure instead and not each value individually.
Again, the performance impact of not doing this is negligible, so measure and make decisions based on that.
In a potential new major version, we are considering swapping the
Context\withValue()
implementation by using aDS\Map
if the extension is available to avoid the performance penalty.
Context
SHOULD NOT contain immutable values
A Context
implementation is already immutable, and it's lifetime does not go outside the request. This means it is
safe for manipulation and free of unexpected side effects.
As long as Context
holds values derived from the request, whose lifetime will also die with it, then it is safe to
store mutable values in it. If you store immutable values and decide that a new reference of that value needs to be
passed down the call stack it means the value should have never been immutable in the first place. You'll have to call
Context\withValue
again and "override" that value.
Services SHOULD NOT be stored inside Context
We greatly discourage storing services inside Context
. It is not a good idea to mix request-scoped values with
application scoped-values like services. Always prefer dependency injection instead of passing services down the
context.
There could be some use cases when this could make sense. For instance, if you need to pass an event loop instance down the call stack to reach some method and make it work asynchronously. I would say that is part of the execution context, although an event loop can be classified as a service.