Context passing

I’m working on another “multi-tenant” PHP web application project and I noticed an interesting series of events. It felt like a natural progression and by means of a bit of dangerous induction, I’m posing the hypothesis that this is how things are just bound to happen in such projects.

In the beginning we start out with a framework that has some authentication functionality built-in. We can get the “current user” from the session, or from some other session-based object. We’ll also need the “current company” (or the “current organization”) of which the current user is a member.

In the web controller, we take this information out of the session (which we can possibly reach through the “request” object passed to the controller action as an argument). Then we start doing the work; making queries, making changes, sending mails, etc. All on behalf of the current user and the current organization. These valuable pieces of information are passed to basically any method, because everything has to happen in the context of the user/company.

Soon this starts to feel like a code smell known as a Data Clump: the same data hanging around together. This results in the first step in the eternal progression of events:

1. Injecting the session

Classes that need the current user, etc. will get the session injected, so they can take the relevant contextual information out of it to do their work.

It may be a bit optimistic to assume that the first solution that comes to mind is injecting the session. I’m afraid that many projects will have some global static method from which you can retrieve the context data.

Injecting the session is almost as bad as getting it from some globally available place. It couples every bit of your project to the framework, which is a bad place to be in (I’ll write about it some other time).

Magic solutions

Talking about frameworks (and libraries) – they usually offer developers some magic alternative solutions. For example, Doctrine ORM has the concept of “filters”, which can automatically modify queries to only return records owned by the current user, company, etc. Of course, these filter classes need to have access to the session too.

I really don’t think that filters are a good solution for the problem at hand – the fact that the query gets modified behind the scenes is nowhere visible in the regular flow of your code, which is dangerous. I favor a more explicit approach here (see below: “Passing contextual values on a need-to-know basis”).

Besides framework coupling, sessions will certainly be annoying to work with if at some point you’ll feel the irrational urge to write tests for your code. At this point, you’ll have to consider different solutions. Back to the Data Clump code smell:

If you always see the same data hanging around together, maybe it belongs together. Consider rolling the related data up into a larger class.

Coding Horror – Code Smells

The suggested solution is to combine that data into a new class. This class often ends up being called…

2. The Context class

It gets instantiated once, after the facts are known, and gets passed along from method to method so you’ll always know where to look for the current user, company, etc.

As a solution, the Context class may remind you of the concept of a value object (from Domain-Driven Design), to combine values that naturally belong together into a conceptual whole. However, there are several things about a Context class that start to smell very quickly.

Entities in the Context

The first thing that ends up in the Context class is the User object, representing the authenticated user. Unfortunately, this user is often modeled as an entity. So now the Context class will contain a complete entity. This way, we expose quite a large API of not only query methods, but also command methods, which would make it possible to modify the state of the User entity that gets passed around. We don’t want this kind of Indecent Exposure. Now every class that retrieves the Context, also implicitly knows about the API of User and sooner or later it will start depending on it. This kind of exposure needs to be prevented at all cost, because it basically couples everything to everything.

By the way, often it’s not only the user, but also the Company entity and maybe some Group, or even a SystemSettings entity that will end up in the Context. Sometimes, all of these things will be created/loaded at the beginning of a request anyway, so by Speculative Generality they will become part of the Context, because “we might need it at some point”.

final class Context
{
    public function getUser(): User
    {
        // ...
    }

    public function getCompany(): Company
    {
        // ...
    }

    // ...
}

So even though the Context class is reminiscent of the concept of a value object, it is by far not a value object, because a value object would never carry around references to (mutable) entities. If you want, there’s a way around this: of course you can write a Context class that is immutable and only contains other value objects or primitive-type values.

Violation of the Interface Segregation Principle

The objects that are passed around as part of the Context all have their own “published interfaces” or APIs. All of the methods of these objects can be called through the Context (although this will lead to another code smell known as a train wreck, e.g. $context->getUser()->getProfile()->getSettings()->preferedNotificationMethod()).

While the combined indirect API of Context is very large, many clients of Context will only be interested in a tiny part of it. This means that the Interface Segregation Principle gets violated. It’s not bad per se to do this, but as I mentioned earlier: clients will start using more methods than would be good for them. Once the interface has been so wide, it will be hard to tighten it.

3.1 Passing contextual values on a need-to-know basis

So after injecting or fetching the session object for a while, then injecting or passing a “context” object, the next logical thing to happen is that we realize that we ended up with some nasty design issues. How to improve the design? Both design issues just described were related to the size of the API that a Context class exposes. The combined API of all the objects it carries is just too large. So, we make it smaller. This will be a two-step process:

  • First, we stop passing around a Context object. We pass only the object(s) that are actually used by a given client.
  • Second, we don’t pass complete entities to former clients of Context. Instead, we pass values (preferably immutable value objects, but optionally primitive-type values).

Since former clients of Context may have had a bit of Inappropriate Intimacy with some of the objects inside it, you may find that it’s hard to make them switch to relying on separately passed values instead. This is the moment when you have more design work to do.

3.2 Fetching more data when needed

That’s where the other half of the final solution comes in: instead of passing values, in some cases it will be better if the client starts using another source to get its information from. This may be a repository or some service, as long as it can answer the same questions as the Context did before:

public function before(Context $context)
{
    $preferedNotificationMethod = $context
        ->getUser()
        ->getProfile()
        ->getSettings()
        ->preferedNotificationMethod();

    // ...
}

public function after(UserId $userId)
{
    $preferedNotificationMethod = $this->settings
        ->preferredNotificationMethod($userId);

    // ...
}

Finally, if at some point none of your classes depend on Context anymore, don’t forget to remove it!

Conclusion

I’m curious about your experiences. Do you have a Context object in your project? Did you feel the need to evolve? Did you find other ways to do so? And what do you think is next?


Source From: php-and-symfony.matthiasnoback.nl.
Original article title: Context passing.
This full article can be read at: Context passing.

Advertisement


Random Article You May Like

Leave a Reply

Your email address will not be published. Required fields are marked *

*
*