Thus far, we have extracted our model domain logic and our view presentation logic. Only two kinds of logic remain in our page scripts:
In this chapter, we will extract a layer of Controller
classes from our page scripts. These will handle the remaining action logic in our legacy application separately from our dependency-creation logic.
For an example of embedded action logic mixed with dependency logic, we can look at the ending example code from the last chapter in Appendix G, Code after Response View File. Therein, we do a little setup work, then we check some conditions and call different parts of our domain Transactions
, and at the end we put together a Response
object to send our response to the client.
As was the problem with mixed-in presentation logic, we cannot test the action logic separately from rest of the page script. Similarly, we cannot easily change the dependency creation logic to make the page script more testable.
We solve the problem of embedded action logic as we did with embedded presentation logic. We must extract the action code to a class of its own to separate the various remaining concerns of our page script. This will also allow us to test the action logic independently from the rest of the application.
Extracting the action logic from our page scripts should be a relatively easy task for us now. Because the domain layer has been extracted, along with the presentation layer, the action logic should be obvious. The work itself still rewards attention to detail, in that the main issue will be picking apart the dependency setup portions from the action logic itself.
In general, the process is as follows:
Controller
class, and modify the page script to use the new Controller
. Spot check the page script with the Controller in place.Controller
class and spot check again.Controller
objects, we are done.At this point, we should be able to find action logic without having to use our project-wide search facility. Every page script in our legacy application probably has at least a little bit of action logic left in it.
When we have a candidate page script, we proceed to rearrange the code so that all setup and dependency-creation work is at the top, all the action logic is in the middle, and the $response->send()
call is at the bottom. For our starting example here, we will use the code from the end of the last chapter as found in Appendix G, Code after Response View File.
First, we go to the very top of the script and place a /* DEPENDENCY */
comment on the first line (or perhaps after the inclusion of a setup script). Then we go to the very end of the script, to the $response->send()
line, and place a /* FINISHED */
comment above it.
Now we reach a point where we must use our professional judgment. On some line after the setup and dependency work in the page script, we will see that the code begins to perform some sort of action logic. Our assessment of just where this transition occurs may be somewhat arbitrary, since the action logic and setup logic are likely to still be intertwined. Even so, we must pick a point at which we believe the action logic really gets started, and place a /* CONTROLLER */
comment there.
Once we have identified these three blocks in the page script, we begin rearranging the code so that only setup and dependency-creation work occurs between /* DEPENDENCY */
and /* CONTROLLER */
, and only action logic occurs between /* CONTROLLER */
and /* FINISHED */
.
In general, we should avoid conditions or loops in the dependency block, and avoid object creation in the controller block. The code in the dependency block should only create objects, and the code in the controller block should only operate on objects that have been created in the dependency block.
Given our starting code in Appendix G, Code after Response View File, we can see the result of an example rearrangement in Appendix H, Code after Controller Rearrangement. Of note, we moved the $user_id
declaration down to the controller block, and we moved the Response
object creation up to the dependency block. The original action logic in the central controller block remains otherwise unchanged.
Finally, after rearranging the page script, we need to spot check our changes to make sure everything still works properly. If we have characterization tests, we should run those. Otherwise, we should browse to or otherwise invoke the page script. If it does not work correctly, we need to undo and redo our rearrangement so that we fix whatever errors we have introduced.
When our spot check runs are successful, we may wish to commit our changes so far. This will give us a known-working state to which we can revert if future changes go bad.
Now that we have a rearranged page script that works properly, we can extract the central controller block to a class of its own. This is not difficult, but we will do it in several sub-steps to make sure everything goes smoothly.
Before we can extract to a class, we need to pick a name for the class we will extract to.
With our domain-layer classes, we chose the top-level namespace Domain. Because this is a controller layer, we will use the top-level namespace Controller. The namespace we use is not as important as consistently using the same namespace for all controllers. Personally, I prefer Controller because it is broad enough to encompass different kinds of controllers, such as Application Controller.
The class name within that namespace should reflect where the page script is in the URL hierarchy, with namespace separators where there are directory separators in the path. This approach makes it obvious what the original page script directory path was, and keeps the subdirectories organized nicely in the class structure. We also suffix the class name with Page
to indicate it is a Page Controller.
For example, if the page script is at /foo/bar/baz.php
, the class name should be Controller\Foo\Bar\BazPage
. The class file itself would then be placed in our central classes directory under classes/Controller/Foo/Bar/BazPage.php
.
Once we have a class name, we can create a skeleton class file for it. We add two empty methods as placeholders for later: the __invoke()
method will receive the action logic from the page script, and the constructor will eventually receive dependencies for the class.
classes/Controller/Foo/Bar/BazPage.php
1 <?php
2 namespace Controller\Foo\Bar;
3
4 class BazPage
5 {
6 public function __construct()
7 {
8 }
9
10 public function __invoke()
11 {
12 }
13 }
14 ?>
Now we are ready to extract the action logic to our new Controller
class.
First, we cut the controller block from the page script, and paste it into the __invoke()
method as-is. We add one line to the end of the action logic, return $response
, to send the Response object back to the calling code.
Next, we go back to the page script. In the place of the extracted action logic, we create an instance of our new Controller
and call its __invoke()
method, getting back a Response object.
We should always
use the same variable name for the Controller object in all of our page scripts. All the examples here will use the name $controller
. This is not because the name $controller
is special, but because this level of consistency will be very important in a later chapter.
At this point, we have successfully decoupled the action logic from the page script. However, this decoupling fundamentally breaks the action logic, because the Controller depends on variables from the page script.
With that in mind, we begin a spot-check-and-modify cycle. We browse to or otherwise invoke the page script and discover that a particular variable is not available to the Controller. We add it to the __invoke()
method signature, and spot check again. We continue adding variables to the __invoke()
method until the Controller has everything it needs and our spot check runs become completely successful.
Given our rearranged page script in Appendix H, Code after Controller Rearrangement, the result of our initial extraction to a Controller can be seen in Appendix I, Code after Controller Extraction. It turns out that the extracted action logic needed four variables: $request
, $response
, $user
, and $article_transactions
.
Once we have a working block of action logic in the __invoke()
method, we will convert the method parameters into constructor parameters so that the Controller can use dependency injection.
First, we cut the __invoke()
parameters and paste them as a whole into the __construct()
parameters. We then edit the class definition and __construct()
method to retain the parameters as properties.
Next, we modify the __invoke()
method to use the class properties instead of the method parameters. That means prefixing each of the needed variables with $this->
.
Then, we go back to the page script. We cut the arguments to the __invoke()
call, and paste them into the Controller instantiation.
Now that we have converted the Controller to dependency injection, we need to spot check the page script again to make sure everything works properly. If it does not, we need to undo and redo our conversion until our tests pass.
At this point, we can remove the /* DEPENDENCY */
, /* CONTROLLER */
, and /* FINISHED */
comments. They have served their purpose and are no longer needed.
Given the __invoke()
usage in Appendix I, Code after Controller Extraction, we can see what converting the Controller to dependency injection looks like in Appendix J, Code after Controller Dependency Injection. We have moved the Controller __invoke()
parameters up to __construct()
, retained them as properties, used the new properties in the __invoke()
method body, and modified the page script to pass the needed variables at new
time instead of __invoke()
time.
Once we have a working page script, we may wish to commit our work yet again so that we have a known correct state to which we can revert later, if needed.
Even though we have tested our page script, we need to write a unit test for our extracted Controller logic. When we write the test, we will need to inject all the needed dependencies into our Controller, preferably as test doubles like fakes or mocks so we can isolate the Controller from the rest of the system.
When we make assertions, they should probably be against the Response object returned from the __invoke()
method. We can use getView()
to make sure the right view file is set, getVars()
to inspect the variables to be used in the view, and getLastCall()
to see if the final callable (if any) has been set properly.
In the examples, we remove all parameters from the __invoke()
method. However, sometimes we will want to pass a parameter to that method as last-minute information for the controller logic.
In general, we should avoid doing so at this point in our modernization process. This is not because it is a poor practice, but because we need a very high level of consistency in our controller invocations for a later modernization step. The most-consistent thing is for there to be no __invoke()
parameters at all.
If we need to pass extra information to the Controller, we should do so via the constructor. This is especially the case when we are passing request values.
For example, instead of this:
page_script.php
1 <?php
2 /* DEPENDENCY */
3 // ...
4 $response = new \Mlaphp\Response('/path/to/app/views');
5 $foo_transactions = new \Domain\Foo\FooTransactions(...);
6 $controller = new \Controller\Foo(
7 $response,
8 $foo_transactions
9 );
10
11 /* CONTROLLER */
12 $response = $controller->__invoke('update', $_POST['user_id']);
13
14 /* FINISHED */
15 $response->send();
16 ?>
We could do this:
page_script.php
1 <?php
2 /* DEPENDENCY */
3 // ...
4 $response = new \Mlaphp\Response('/path/to/app/views');
5 $foo_transactions = new \Domain\Foo\FooTransactions(...);
6 $request = new \Mlaphp\Request($GLOBALS);
7 $controller = new \Controller\Foo(
8 $response,
9 $foo_transactions,
10 $request
11 );
12
13 /* CONTROLLER */
14 $response = $controller->__invoke();
15
16 /* FINISHED */
17 $response->send();
18 ?>
The __invoke()
method body would then use $this->request->get['item_id']
.
In the examples, our Controller objects perform a single action. However, it is often the case that a page controller encompasses multiple actions, such as both inserting and updating a database record.
Our first pass at extracting action logic from the page script should keep the code pretty much intact, making allowances for properties instead of local variables and so on. Once the code is in the class, though, it is perfectly reasonable to split the logic into separate action methods. Then the __invoke()
method can become little more than a switch
statement that picks the correct action method. If we do so, we should be sure to update our Controller tests, and continue to spot check the page script to make sure our changes do not break anything.
Note that if we create additional Controller action methods, we need to avoid calling them from our page script. For the sake of the consistency needed in a later modernization step, the __invoke()
method should be the only Controller method our page script calls in its controller block.
Unfortunately, as we go about rearranging a page script, we are likely to discover that we still have several include
calls in the controller block. (Calls to include
for setup and dependency purposes are not such a big deal, especially if they are the same in every page script.)
Having include
calls in the controller block is an artifact of the include-oriented architecture with which our legacy application began. It is a particularly difficult problem to solve. We want to encapsulate action logic in classes, not in files that execute behavior the moment we include
them.
For now, we must submit ourselves to the idea that include
calls in the controller block of our page scripts are ugly but necessary. We should avert our eyes if needed and copy them into the Controller
class with the rest of the controller code from the page script.
As consolation, we will solve the problem of these embedded include
calls in the next chapter.