PHP; Static Methods Are Evil!
In this article, I will show you how PHP static methods, more often than not, result in coupled code that is hard to test. Certainly, it is possible to write clean, decoupled, maintainable and easy to test code using static methods. However, in the real world, I have never seen this. The amusing part about static methods is that the problems come up, not in these methods themselves but in the code calling them.
But first, let’s lay down some foundation.
Domain Layer
In short, the idea is to separate the business logic (domain) from the application logic (framework) as much as possible. Most of the production applications I have worked with were so tightly coupled to the framework that any attempt at moving to a different framework was equivalent with a significant rewrite. How nice would it be if it was possible to move an entire app from Phalcon to Laravel in a week? The same principle applies when switching from a traditional web application to a single page application or even native mobile apps.
Free the domain
This part is actually very easy. Only things that are specific to the implementation should exist in the application layer. These include routing, caching, database, queues, etc… Things that aren’t, go into your domain code. I like to think about the domain in terms of independent modules that are responsible for very narrow and focused part of the business. Each little piece needs to remain decoupled from the rest; the framework will function as the glue tying it all together.
Example
Let’s look at what might happen when an API call comes in to post a tweet.
- Our application layer (framework) receives the HTTP requests and invokes an appropriate method in the
TweetController
- The controller instantiates a
Tweet
model that exists in our domain and is completely agnostic to the framework models, i.e. theTweet
model does not inherit from or implement any code from the application layer. - The
Tweet
model checks if it is 140 characters or less (business logic). - The
TweetController
asks theTweetRepository
to save theTweet
to the database.TweetRepository
exists in the application layer because it handles tasks that are specific to our implementation like database and caching.
Dependency Injection
DI is that glue tying all pieces together that I mentioned above. Most of the frameworks will provide this in one way or another. The best approach is to specify interfaces your class depends on in the constructor of your class and allow the framework to inject concrete instances. By using the interfaces we prevent the class from being coupled to any one specific implementation. Let’s consider the TweetRepository
I mentioned above.
class TweetRepository
{
protected $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
}
As you can see this class has a dependency on an implementation of the CacheInterface
. Our controller will have a dependency on the TweetRepositoryInterface
and the instance of this class will be injected by the framework.
K. Y tho?
In theory, decoupled modules are much more malleable and easier to be moved to a different project. In practice, it comes down to testing, maintainability, agility, and testing. But most importantly, testing. Actually, let’s put it in bigger letters.
Testing
It is much easier to test code that has no concrete dependencies by injecting mocks. I don’t need Redis running to make sure that the TweetRepository
is working by making sure that it makes the correct calls to our mock. I should be able to take a class and its test and run them in complete isolation, without the framework or any other modules from the domain.
Production vs. Development
Another huge benefit is the ability to run the code differently in different environments. For example, I can configure the framework to use RedisCache
as our CacheInterface
on production but for local, I can use ArrayCache
with just a change to .env
file.
What about the static methods?
It sounds great and all but you still might be wondering what static methods have to do with any of that. Let’s take a look at this example.
class TweetRepository
{
public function get($id)
{
Logger::log(‘Getting tweet’, $id);
}
}
The issue here is that the TweetRepository
is tightly coupled to Logger
.
Limitation: final, private, and static methods
Please note that final, private and static methods cannot be stubbed or mocked. They are ignored by PHPUnit’s test double functionality and retain their original behavior.
This means that in order to test the TweetRepository
we also have to have Logger
available. It can get much worse if said Logger
is writing things to the database. We are unable to mock any of that, making testing this class very difficult and not very reliable. Any issue down the chain would cause test errors, even though TweetRepository
has no issues itself.
Additionally, I can’t replace the implementation of Logger
easily, making it harder to set this application up for local development.