8000 [12.x] Introduce `Contextable` by cosmastech · Pull Request #57325 · laravel/framework · GitHub
[go: up one dir, main page]

Skip to content

Conversation

cosmastech
Copy link
Contributor
@cosmastech cosmastech commented Oct 9, 2025

Short summary

This is a simply way to create an object that syncs with the logging output.

Why? Dear god, why?

In a project I worked on semi-recently, we needed a "RequestContext" state that we stored. It was a class that was registered as a singleton in the container. A middleware would store the request data in the database, and then it would also keep a few other bits of data. Every time we updated the object via setters, it would re-sync itself to the Context facade.

In order to keep audit tracking, values from the request context was used in a number of inserts, hence why it was so meaningful to have it be a singleton.

Ok... maybe, but how do I use it.

It's pretty easy, just make yourself a Contextable object.

class RequestContext implements Contextable
{
    public function __construct(
        public ApiRequestModel $apiRequest,
        public AuthType $authType
    ) {}

    #[Override]
    public function context($repository)
    {
        return [
            'requesting_app' => $this->apiRequest->app_id,
            'auth_type' => $this->authType->value,
            'request_id' => $this->apiRequest->id,
        ];
    }
}

// then in a middleware or whatever
$requestContext = new RequestContext(ApiRequestModel::capture($request), AuthType::BEARER_TOKEN);

app()->instance(RequestContext::class, $requestContext);
Context::add($requestContext);

// Then in my code, all of my logs will include the requesting_app, auth_type, and request_id data 😃

(Note: I fully expect Taylor to hate the idea of using an interface here, and instead we could just check that a method called contextData() exists on the Contextable. No problem)

@shaedrich
Copy link
Contributor

(Note: I fully expect Taylor to hate the idea of using an interface here, and instead we could just check that a method called contextData() exists on the Contextable. No problem)

Alternatively, this could be an attribute

@taylorotwell
Copy link
Member

Hey @cosmastech - am I missing something in your example where you actually add it to the context?

@cosmastech
Copy link
Contributor Author
cosmastech commented Oct 9, 2025

Hey @cosmastech - am I missing something in your example where you actually add it to the context?

Holy cow, yeah I did forget that.

But we could automatically store it in after resolving

Edit: updated the example

@timacdonald
Copy link
Member
timacdonald commented Oct 9, 2025
  1. I don't love that from a public API perspective, there is now public, hidden, and contextable data.

  2. I wonder if you should be able to return public and hidden data from context? Going further, rather than only allowing the addition of public data, I wonder if you could just pass the context repository into the method itself and allow the object to determine at runtime what it wants to do.

interface Contextable
{
    public function withContext($context)
    {
        if ($context->has('foo')) {
            return;
        }

        $context->add('foo', $this->foo);
    }
}
  1. I worry about serializing the Contextable object when passing to the queue. It could contain a lot of irrelevant nested data with only a single key value pair relevant to context. With the current design we'd have to serialize the entire thing just to bring across a potentially static key value pair. With my proposed approach, we don't need to serialize the Contextable object. We take the approach that whatever data is available in Context when it is serialized it what will appear on the other side.

On the other side, if you wanted the data to continue to be dynamic, you can always use the Context::dehyrating hook to configure that.

I do note that this approach only works for objects you control. For objects you don't own, I suppose there is also a hooks style approach we could take, e.g.,

public function show(Model $model)
{
    Context::whenRetreiving(fn () => Context::add('foo', $model->foo));
}

@timacdonald
Copy link
Member

FWIW, my initial implementation of Context supported dynamic data via Closures. I got it really close but then ended up stripping it all out as I ran into issues that were blocking my shipping the thing and were not worth the time, at the time. All that is to say, I like the idea!

@cosmastech
Copy link
Contributor Author
  1. I don't love that from a public API perspective, there is now public, hidden, and contextable data.

What concerns do you have? Just the cognitive overhead? Do you think it's ugly, or truly something that makes it harder for developers to use?

I look at Context's public data as being something I want written to my logs, and the hidden data being something I want to keep accessible from multiple places in my app without having to create a data container class.

We could possible just modify add() so that when it receives a Contextable instance as a parameter, it stores that separately. That makes it easier for user-land to just keep their arms around one function.

  1. I wonder if you should be able to return public and hidden data from context? Going further, rather than only allowing the addition of public data, I wonder if you could just pass the context repository into the method itself and allow the object to determine at runtime what it wants to do.
interface Contextable
{
    public function withContext($context)
    {
        if ($context->has('foo')) {
            return;
        }

        $context->add('foo', $this->foo);
    }
}

I think passing the Repository instance as an argument makes sense. I am not personally a fan of having the Contextable get/set its own values into the Context though. This feels like it makes this significantly less user-friendly. Perhaps if the withContext() (or contextData() as I named it initially) doesn't return an iterable, we can kind of just skip over merging those values. I think that allows for flexibility, while making it very user friendly.

Were you proposing a second method on the class, or withContext() replacing contextData()? You had also mentioned calling it context() above. I am ok with any name, just wanted to make sure I was following.

  1. I worry about serializing the Contextable object when passing to the queue. It could contain a lot of irrelevant nested data with only a single key value pair relevant to context. With the current design we'd have to serialize the entire thing just to bring across a potentially static key value pair. With my proposed approach, we don't need to serialize the Contextable object. We take the approach that whatever data is available in Context when it is serialized it what will appear on the other side.

For the use-case I had mentioned in the original post, I think that this unfortunately saps a lot of the usefulness out of this functionality. To me, it feels like restoring the object to its original state would be easiest to use. (This current implementation doesn't account for re-binding it as a singleton in the container though... which was how we were using it).

In the project, we were storing manipulating file uploads via a dispatched job. The dispatched job would produce a PDF and store some metadata about it (like which app/user created it, on multiple tables). Otherwise, we have to fetch those values out of the Context's data, which leads to magic strings and untyped properties.

On the other side, if you wanted the data to continue to be dynamic, you can always use the Context::dehyrating hook to configure that.

Yeah, that's what we had done in the project I was referencing. It's not particularly clean.

I do note that this approach only works for objects you control. For objects you don't own, I suppose there is also a hooks style approach we could take, e.g.,

public function show(Model $model)
{
    Context::whenRetreiving(fn () => Context::add('foo', $model->foo));
}

I'm not sure I can picture this use-case very well of an object you don't own being something you'd want to keep in Context? Could you provide me with a for instance here?

@cosmastech cosmastech marked this pull request as draft October 10, 2025 02:20
@cosmastech
Copy link
Contributor Author

FWIW, my initial implementation of Context supported dynamic data via Closures. I got it really close but then ended up stripping it all out as I ran into issues that were blocking my shipping the thing and were not worth the time, at the time. All that is to say, I like the idea!

I asked some questions above that I would love to get your thoughts on.

As always @timacdonald, I really appreciate your feedback and encouragement. 🙇

@cosmastech cosmastech marked this pull request as ready for review October 10, 2025 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

0