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.
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?
Excellent article! I built a multi-tenant system last year and blogged about this very same issue, although your article is way more well developed. I wrote a tiny library to serve as a context passing abstraction that I would love to have your feedback on. Cheers!
Great read. The problem you have highlighted is real and common. I thoroughly agree with all you said about core of the problem. It is not explicitly said but I think your solution applies to controller/handler/service usages. I think that injecting services to entities/aggregates is considered harmfull. Both are all about managing internal state so outer services just pollutes them. Also it makes it harder to test or even reconstitiute. Can you clarify this point?
Thanks! Yes, injecting services into aggregates is generally not necessary, but it's "allowed" in some cases (my favorite example is when the aggregate needs to hash a plain text password passed to it, and receives a hashing service to do so, without knowing about the hashing algorithm).
I personally prefer double dispatch approach in such cases. It comes with two advantages. It's intention revealing - you have to pass hashing algoritm only in cases it is necessary so client code author understand why. Often most of entity methods does not need it. Secondly, entity/aggregate remains pure and easy to create/reconstitiute and can itself focus on managing own state and protecting invariants.
What do you think about that?
Well, only if
$hashingAlgorithm
represents a service, typed by its interface. But indeed, that's a double dispatch - say the interface has a methodhashPassword()
, theUser
will call that method to actually do the hashing.I thought you suggest injecting hashingAlgoritm as constructor dependency ;)
I would never ;)
Interesting twist on this, which I noticed this weekend: the long-time Mgo golang community driver for MongoDB had no context object, but the newly-designed official MongoDB driver for golang has this context all around. And in a different context (!), React just ended up officializing the use of the context. Food for thought ?
Interesting indeed; by the way, I'm not implying that anything called "context" isn't okay. However, it's often a smelly thing, because it's such a generic term, anything could fit in the concept (just like "manager").
But, as explained in the article, passing a "Context" is often the next best thing after people have previously been depending on some global state. At least, the global state will now be confined to some local "context", which is much better.
We use a microservice based approach (with stateless services) where every service has its own responsibilities, so lets say we have a Email service that needs an username and a company name. we expect the client or whatever source to provide those pieces of information.
So the client first requests the username and company name from various designated services and sends the to the email service.
i use similar pattern but:
avoid global context objects
i create a context object for each service i use such a context object. it's not a global context.
just store scalar values or IdObjects (aka. keep it serializeable/sendable)
for example i would not store a company or a user object, i would return a CompanyReferenceInterface (with a getCompanyId method) and a UserReferenceInterface (with a getUserId method).
if you need an instance of for example the company, just pass the ReferenceInterface to the companyService. The Company-Entity could implement the CompanyReferenceInterface . So the company Service could optimize this call to just return the companyObject :)
Very good advice and thank you for taking the time to write about this. We have something similar in our app, where we started creating an implementation of "InitiatingUserInterface", which we use, to know who is actually accessing the web app. It's some kind of Session object (but it's not) but more advanced which can have different implementation depending on a specific role or context! We like this in the sense that it regroups the information we need regarding the user executing the requests. That said, as you described, in the beginning this object was injected in every service or class that required a user, or the company, or something else. In the end, this InitiatingUserInterface was coupled to almost every service class. Just because it was easy :)
Now we are stepping away from that, by just injecting what is really required by the services and only use the InitiatingUserInterface implementation in the Controller area.
The only thing which was not so bad, is that InitiatingUserInterface is something we created and not bounded to the framework, so it's not complex to extract, and it's an interface. But still, it's not an excuse to provide too much to a service/client if only it requires a small subset of it.
Cheers
Thanks Toni. Yes, the solution you describe is much better than directly depending on framework stuff. This way, testing will be already a lot easier, and your design is flexible. But yes, there's a next step after that (which may or may not bring you the value you'd want in return for the amount of work required ;)).