In this blog, we discuss the current reality of PHP in 2021, and offer projections for the future of PHP — both as a language and as an ecosystem.
This article is an excerpt from my book Advanced Web Application Architecture. It contains a couple of sections from the conclusion of Part I: Decoupling from infrastructure.
This chapter covers:
- A deeper discussion on the distinction between core and
- A summary of the strategy for pushing infrastructure to the sides
- A recommendation for using a domain- and test-first approach to
- A closer look at the concept of “pure” object-oriented programming
Core code and infrastructure code
In Chapter 1 we’ve looked
at definitions for the terms core code and infrastructure code. What
I personally find useful about these definitions is that you can look at
a piece of code and find out if the definitions apply to it. You can
then decide if it’s either core or infrastructure code. But there are
other ways of applying these terms to software. One way is to consider
the bigger picture of the application and its interactions with
actors. You’ll find the term actor in books about user stories and use
cases by authors like Ivar Jacobson and Alistair Cockburn, who make a
- Primary actors, which act upon our system
- Secondary or supporting actors, upon which our system acts
As an example, a primary actor could be a person using their web
browser to send an HTTP
POST request to our application. A
supporting actor could be the relational database that our application
sends an SQL
INSERT query to. Communicating with both actors requires
many infrastructural elements to be in place. The web server should be
up an running, and it should be accessible from the internet. The server
needs to pass incoming requests to the application, which likely uses a
web framework to process the HTTP messages and dispatch them to the
right controllers. On the other end of the application some data may
have to be stored in the database. PHP needs to have a PDO driver
installed before it can connect to and communicate with the database.
Most likely you’ll need a lot of supporting code as well to do the
mapping from domain objects to database records. All of the code
involved in this process, including a lot of third-party libraries and
frameworks, as well as software that isn’t maintained by yourself (like
the web server), should be considered infrastructure code.
Most of the time between the primary actor sending an HTTP request to
your server, and the database storing the modified data, will be spent
by running infrastructure code and most of this code can be found in PHP
extensions, frameworks, and libraries. But somewhere between ingoing and
outgoing communication the server will call some of your own code, the
so-called user code.
User code is what makes your application special: what things can you
do with your application? You can order an e-book. You can pay for it.
What kind of things can you learn from your application? You can see
what e-books are available. And once you’ve bought one, you can download
it. Frameworks, libraries, and PHP extensions could never help you with
this kind of code, because it’s domain-specific: it’s your business
The following figure shows that user
code is in the middle of a lot of infrastructure code:
Even if we try to
ignore most of the surrounding infrastructure while working on and
testing user code, we’ll often find that this code is hard to work with.
That’s because the code still contains many infrastructural details. A
use case may be inseparable from the web controller that invokes it. The
use of service locators and the likes prevents code from running in
isolation, or in a different context. Calls to external services require
the external service to be available when we want to locally test our
code. And so on…
If that’s the case, user code consists of a mix of infrastructure code
and core code.
The following figure shows what this looks like:
When I look at this diagram, I immediately feel
the urge to push the bits of infrastructure co
Truncated by Planet PHP, read more at the original (another 12319 bytes)
A lot can happen in 9 years. Back then I was still advocating that you should unit-test your controllers and that setter injection is very helpful when replacing controller dependencies with test doubles. I’ve changed my mind: constructor injection is the right way for any service object, including controllers. And controllers shouldn’t be unit tested, because:
- Those unit tests tend to be a one-to-one copy of the controller code itself. There is no healthy distance between the test and the implementation.
- Controllers need some form of integrated testing, because by zooming in on the class-level, you don’t know if the controller will behave well when the application is actually used. Is the routing configuration correct? Can the framework resolve all of the controller’s arguments? Will dependencies be injected properly? And so on.
The alternative I mentioned in 2012 is to write functional tests for your controller. But this is not preferable in the end. These tests are slow and fragile, because you end up invoking much more code than just the domain logic.
Ports and adapters
If you’re using a decoupled approach, you can already test your domain logic using fast, stable, coarse-grained unit tests. So you particularly don’t want to let your controller tests also invoke domain logic. You only want to verify that the controller correctly invokes your domain logic. We’ve seen one approach in Talk review: Thomas Pierrain at DDD Africa, where Thomas explained how he includes controller logic in his coarse-grained unit tests. He also mentioned that it’s not “by the book”, so here I’d like to take the time to explain what by the book would look like.
Hexagonal architecture prescribes that all application ports should be interfaces. That’s because right-side ports should potentially have more than one adapter. The port, being an interface, allows you to define a contract for communicating with external services. On the left side, the ports should be interfaces too, because this allows you to replace the port with a mock when testing the left-side adapter.
Following the example from my previous post, this is a schema of the use case “purchasing an e-book”:
PurchaseRepositoryUsingSql are adapter classes (which need supporting code from frameworks, libraries, etc.). On the left side, the adapter takes the request data and uses it to determine how to call the
PurchaseEbookService, which represents the port. On the right side there is another port: the
PurchaseRepository. One of its adapters is
PurchaseRepositoryUsingSql. The following diagram shows how this setup allows you to invoke the left-side port from a test runner, without any problem, while you can replace the right-side port adapter with a fake repository:
Left-side adapter tests
Since the test case replaces the controller, the controller remains untested. Even though the controller should be only a few lines of code, there may be problems hiding there that will only be uncovered by exploratory (browser) testing or once the application has been deployed to production.
In this case it would help to create an adapter test for the controller. This is not a unit test, but an integrated test. We don’t invoke the controller object directly, but travel the normal route from browser request to response, through the web server, the framework, and back. This ensures that we got everything right, and leave (almost) no room for mistakes in interpreting framework configuration (routing, security, dependency injection, etc.).
We have already established that you wouldn’t want to test domain logic again through a web request. Two things we need to solve for adapter tests then:
- The controller shouldn’t invoke the domain logic. Instead, we should only verify that it calls the port (which is an interface) in the right way. The pattern for this is called mocking: we need a test double that records the calls made to it and makes assertions about those calls (the number of times, and the arguments provided).
- We need a way to inject this mock into the controller as a constructor argument. Thi
Truncated by Planet PHP, read more at the original (another 7582 bytes)