The difficulty of extracting includes to their own classes depends on the number and complexity of the include
calls remaining in our class files. If there are very few includes and they are relatively simple, the process will be easy to complete. If there are many complex interdependent includes, the process will be relatively difficult to work through.
In general, the process is as follows:
classes/
directory for an include
call in a class.include
call, search the entire codebase to find how many times the included file is used.include
call.new
, inject dependencies, return instead of output, and no include
calls.include
call with inline instantiation of the new class and invocation of the new method.include
was replaced to find coupled variables; add these to the new method signature by reference.include
calls to that same file, and replace each with inline instantiation and invocation; spot check modified files and test modified classes.include
file; unit test and spot check the entire legacy application.new
, inject dependencies, return-not-output, and no includes.include
calls in any of our classes.First, as we did in a much earlier chapter, we use our project-wide search facility to find include
calls. In this case, search only the classes/
directory with the following regular expression:
^[ \t]*(include|include_once|require|require_once)
This should give us a list of candidate include
calls in the classes/
directory.
We pick a single include
file to work with, then search the entire codebase for other inclusions of the same file. For example, if we found this candidate include
...
1 <?php 2 require 'foo/bar/baz.php'; 3 ?>
We would search the entire codebase for include
calls to the file name baz.php
:
^[ \t]*(include|include_once|require|require_once).*baz\.php
We search only for the file name because, depending on where the include
call is located, the relative directory paths might lead to the same file. It is up to us to determine which of these include
calls reference the same file.
Once we have a list of include
calls that we know lead to the same file, we count the number of calls that include that file. If there is only one call, our work is relatively simple. If there is more than one call, our work is more complex.
If a file is used as the target of an include
call only once, it is relatively easy to remove the include
.
First, we copy the entire contents of the include
file. We move back to the class where the include
occurs, delete the include
call, and paste the entire contents of the include
file in its place.
Next, we run the unit tests for the class to make sure it still works properly. If they fail, we rejoice! We have found errors to be corrected before we continue. If they pass, we likewise rejoice, and move on.
Now that the include
call has been replaced, and the file contents have been successfully transplanted to the class, we delete the include file. It is no longer needed.
Finally, we can return to our class file where the newly transplanted code lives. We refactor it according to all the rules we have learned so far: no globals or superglobals, no use of the new
keyword outside of factories, inject all needed dependencies, return values instead of generating output, and (recursively) no include
calls. We run our unit tests along the way to make sure we do not break any pre-existing functionality.
First, we will copy the include
code to a class method of its own. To do this, we need to pick a class name appropriate to the purpose of the included file. Alternatively, we may name the class based on the path to the included file so we can keep track of where the code came from originally.
As for the method name, we again pick something appropriate to the purpose of the include
code. Personally, if the class is going to contain only a single method, I like to co-opt the __invoke()
method for this. However, if there end up being multiple methods, we need to pick a sensible name for each one.
Once we have picked a class name and method, we create the new class in the proper file location, and copy the include
code directly into the new method. (We do not delete the include file itself just yet.)
Now that we have a class to work with, we go back to the include
call we discovered in our search, replace it with an inline instantiation of the new class, and invoke the new method.
For example, say the original calling code looked like this:
Calling Code
1 <?php
2 // ...
3 include 'includes/validators/validate_new_user.php';
4 // ...
5 ?>
If we extracted the include
code to a Validator\NewUserValidator
class as its __invoke()
method body, we might replace the include
call with this:
Calling Code
1 <?php
2 // ...
3 $validator = new \Validator\NewUserValidator;
4 $validator->__invoke();
5 // ...
6 ?>
We have now successfully decoupled the calling code from the include
file, but this leaves us with a problem. Because the calling code executed the include
code inline, the variables needed by the newly-extracted code are no longer available. We need to pass into the new class method all the variables it needs for execution, and to make its variables available to the calling code when the method is done.
To do so, we run the unit tests for the class that called the include
. The tests will reveal to us what variables are needed by the new method. We can then pass these into the method by reference. Using a reference makes sure that both blocks of code are operating on the exact same variables, just as if the include
was still being executed inline. This minimizes the number of changes we need to make to the calling code and the newly extracted code.
For example, say we have extracted the code from an include
file to this class and method:
classes/Validator/NewUserValidator.php
1 <?php
2 namespace Validator;
3
4 class NewUserValidator
5 {
6 public function __invoke()
7 {
8 $user_messages = array();
9 $user_is_valid = true;
10
11 if (! Validate::email($user['email'])) {
12 $user_messages[] = 'Email is not valid.';
13 $user_is_valid = false;
14 }
15
16 if (! Validate::strlen($foo['username'], 6, 8)) {
17 $user_messages[] = 'Username must be 6-8 characters long.';
18 $user_is_valid = false;
19 }
20
21 if ($user['password'] !== $user['confirm_password']) {
22 $user_messages[] = 'Passwords do not match.';
23 $user_is_valid = false;
24 }
25 }
26 }
27 ?>
When we test the class that calls this code in place of an include
, the tests will fail, because the $user
value is not available to the new method, and the $user_messages
and $user_is_valid
variables are not available to the calling code. We rejoice at the failure, because it tells us what we need to do next! We add each missing variable to the method signature by reference:
classes/Validator/NewUserValidator.php
1 <?php
2 public function __invoke(&$user, &$user_messages, &$user_is_valid)
3 ?>
We then pass the variables to the method from the calling code:
classes/Validator/NewUserValidator.php
1 <?php
2 $validator->__invoke($user, $user_messages, $user_is_valid);
3 ?>
We continue running the unit tests until they all pass, adding variables as needed. When all the tests pass, we rejoice! All the needed variables are now available in both scopes, and the code itself will remain decoupled and testable.
Now that we have decoupled our original calling code from the include
file, we need to decouple all other remaining code from the same file. Given our earlier search results, we go to each file and replace the relevant include
call with an inline instantiation of the new class. We then add a line that calls the new method with the needed variables.
Note that we may be replacing code within classes, or within non-class files such as view files. If we replace code in a class, we should run the unit tests for that class to make sure the replacement does not break anything. If we replace code in a non-class file, we should run the test for that file if it exists (such as a view file test), or else spot check the file if no tests exist for it.
Once we have replaced all include
calls to the file, we delete the file. We should now run all of our tests and spot checks for the entire legacy application to make sure that we did not miss an include
call to that file. If a test or spot check fails, we need to remedy it before continuing.
Now that the legacy application works just as it used to before we extracted the include
code to its own class, we write a unit test for the new class.
Once we have a passing unit test for the new class, we refactor the code in that class according to all the rules we have learned so far: no globals or superglobals, no use of the new
keyword outside of factories, inject all needed dependencies, return values instead of generating output, and (recursively) no include
calls. We continue to run our tests along the way to make sure we do not break any pre-existing functionality.
When the unit test for our newly refactored class passes, we proceed to replace all our inline instantiations with dependency injection. We do so only in our class files; in our view files and other non-class files, the inline instantiation is not much of a problem
For example, we may see this inline instantiation and invocation in a class:
classes/Controller/NewUserPage.php
1 <?php
2 namespace Controller;
3
4 class NewUserPage
5 {
6 // ...
7
8 public function __invoke()
9 {
10 // ...
11 $user = $this->request->post['user'];
12
13 $validator = new \Validator\NewUserValidator;
14 $validator->__invoke($user, $user_messages, $u
15
16 if ($user_is_valid) {
17 $this->user_transactions->addNewUser($user
18 $this->response->setVars('success' => true
19 } else {
20 $this->response->setVars(array(
21 'success' => false,
22 'user_messages' => $user_messages
23 ));
24 }
25
26 return $this->response;
27 }
28 }
29 ?>
We move the $validator
to a property injected via the constructor, and use the property in the method:
classes/Controller/NewUserPage.php
1 <?php
2 namespace Controller;
3
4 class NewUserPage
5 {
6 // ...
7
8 public function __construct(
9 \Mlaphp\Request $request,
10 \Mlaphp\Response $response,
11 \Domain\Users\UserTransactions $user_transactions,
12 \Validator\NewUserValidator $validator
13 ) {
14 $this->request = $request;
15 $this->response = $response;
16 $this->user_transactions = $user_transactions;
17 $this->validator = $validator;
18 }
19
20 public function __invoke()
21 {
22 // ...
23 $user = $this->request->post['user'];
24
25 $this->validator->__invoke($user, $user_messages, $user_is_valid);
26
27 if ($user_is_valid) {
28 $this->user_transactions->addNewUser($user);
29 $this->response->setVars('success' => true);
30 } else {
31 $this->response->setVars(array(
32 'success' => false,
33 'user_messages' => $user_messages
34 ));
35 }
36
37 return $this->response;
38 }
39 }
40 ?>
Now we need to search the codebase and replace every instantiation of the modified class to pass the new dependency object. We run our tests as we go to make sure everything continues to operate properly.
In the examples, we show the include
code being extracted to a class by itself. If we have many related include
files, it may be reasonable to collect them into the same class, each with their own method name. For example, the NewUserValidator logic might be only one of many user-related validators. We can reasonably imagine the class renamed as UserValidator with such methods as validateNewUser()
, validateExistingUser()
, and so on.
In our search for include
calls, we look only in the classes/
directory for the originating calls. It is likely that there are include
calls that originate from other locations as well, such as the views/
.
For the purposes of our refactoring, we don't particularly care about include
calls that originate outside our classes. If an include
is called only from non-class files, we can safely leave that include
in its existing state.
Our main goal here is to remove include
calls from class files, not necessarily from the entire legacy application. At this point, it is likely that most or all include
calls outside our classes are part of the presentation logic anyway.