For an example of embedded presentation logic, we can take a look at Appendix E, Code before Collecting.
Presentation Logic. The code shows a page script that has been refactored to use domain Transactions, but it still has some presentation logic entangled within the rest of the code.
What Is The Difference Between Presentation and Business Logic?
For our purposes, presentation logic includes any and all code that generates output sent to the user (such as a browser or mobile client). This includes not only echo
and print
but also header()
and setcookie()
. Each of these generates some form of output. "Business logic," on the other hand, is everything else.
The key to decoupling the presentation logic from the business logic is to put the code for them into separate scopes. The script should first perform all of the business logic, then pass the results over to the presentation logic. When that is complete, we will be able to test our presentation logic separately from our business logic.
To achieve this separation of scope, we will move toward using a Response
object in our page scripts. All of our presentation logic will be executed from within a Response
instance, instead of directly in the page script. Doing so will provide the scope separation that we need to decouple all output generation, including HTTP headers and cookies, from the rest of the page script.
Often, when we think of presentation, we think of a view or a template system that renders content for us. However, these kinds of systems do not usually encapsulate the full set of output that will be sent to the user. We need to output not just HTTP bodies, but HTTP headers as well. In addition, we need to be able to test that the correct headers have been set, and that the content has been generated properly. As such, the Response
object is a better fit at this point than a view or template system alone. For our Response
object, we will use the class provided at http://mlaphp.com/code. Note that we will be including files within a Response context, which means that the methods on that object will be available to include
files running "inside" that object.
Extracting presentation logic is not as difficult as extracting domain logic. However, it does require careful attention and lots of testing along the way.
In general, the process is as follows:
Response
, and spot check the script again to make sure the script works correctly with the new Response
.In general, it should be easy for us to find presentation logic in our legacy application. At this point we should be familiar enough with the codebase to have a good idea where the page scripts generate output.
Now that we have a candidate page script, we need to rearrange the code so there is a clear demarcation between the presentation logic and everything else. For our example here, we will use the code in Appendix E, Code before Collecting.
First, we go to the bottom of the file and add a /* PRESENTATION */
comment as the final line. We then go back to the top of the file. Working line-by-line and block-by-block, we move all presentation logic to the end of the file after our /* PRESENTATION */
comment. When we are done, the part before the /* PRESENTATION */
comment should consist only of business logic, and the part after should consist only of presentation logic.
Given our starting code in Appendix E, Code before Collecting, we should end up with something more like the code in Appendix F, Code after Collecting. In particular, note that we have the following:
$current_page
, down the presentation blockheader.php
include down to the presentation blockif
that sets the $page_title
, to the presentation block$_SERVER['PHP_SELF']
with an $action
variable$_GET['id']
with an $id
variableNow that we have rearranged the page script so that all presentation logic is collected at the end, we need to spot check to make sure the page script still works properly. As usual, we do this by running our pre-existing characterization tests, if we have any. If not, we must browse to or otherwise invoke the changed code.
Now that we have a working page script with all the presentation logic in a single block, we will extract that entire block to its own file, and then use a Response
to execute the extracted logic.
First, we need a place to put view files in our legacy application. While I prefer to keep presentation logic near the business logic, that kind of arrangement will make trouble for us in later modernization steps. As such, we will create a new directory in our legacy application called views/
and place our view files there. This directory should be at the same level as our classes/
and tests/
directories. For example:
Now that we have a place to save our view files, we need to pick a file name for the presentation logic we are about to extract. The view file should be named for the page script, in a path under views/
that matches the page script path. For example, if we are extracting presentation from a page script at /foo/bar/baz.php
, the target view file should be saved at /views/foo/bar/baz.php
.
Sometimes it is useful to use an extension other than just .php
for our view files. I have found it can be helpful to use an extension that indicates the view format. For example, a view that generates HTML may end in .html.php
, while a view that generates JSON may end in .json.php
.
Next, we cut the presentation block from the page script, and paste it into our new view file as-is.
Given our earlier examples in Appendix E, Code before Collecting and Appendix F, Code after Collecting, we end up at Appendix G, Code after Response View File. We can see that the articles.html.php
view file needed four variables: $id, $failure
, $input
, and $action
:
1 <?php 2 // ... 3 $response->setVars(array( 4 'id' => $id, 5 'failure' => $article_transactions->getFailure(), 6 'input' => $article_transactions->getInput(), 7 'action' => $_SERVER['PHP_SELF'], 8 )); 9 // ... 10 ?>
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.
Unfortunately, most legacy applications pay little or no attention to output security. One of the most common vulnerabilities is cross-site scripting (XSS).
Cross-site scripting is an attack that is made possible by user input being sent back to the browser unescaped. For example, an attacker can enter maliciously-crafted JavaScript code into a form input or an HTTP header. If that value is then delivered back to the browser without being escaped, the browser will execute that JavaScript code. This has the potential to open the client browser to further attacks. For more information, please see the OWASP entry on XSS (https://www.owasp.org/index.php/Cross-site_Scripting_%28XSS%29).
The defense against XSS is to escape all variables all the time for the context in which they are used. If a variable is used as HTML content, it needs to be escaped as HTML content; if a variable is used in an HTML attribute, it needs to be escaped as such, and so on.
Defending against XSS requires diligence on the part of the developer. If we remember one thing about escaping output, it should be the htmlspecialchars()
function. Using this function appropriately will save us from most, but not all, XSS exploits.
This needs to be escaped like this:
Unfortunately, it is outside the scope of this book to provide a thorough overview of escaping and other security techniques. For more information, and for a good stand-alone escaping tool, please see the Zend\Escaper (https://framework.zend.com/manual/2.2/en/modules/zend.escaper) library.
After we escape all output in the Response
view file, we can move along to testing.
Writing tests for view files presents some unique challenges. Until this chapter, all of our tests have been against classes and class methods. Because our view files are, well, files, we need to place them into a slightly different testing structure.
First, we need to create a views/
subdirectory in our tests/
directory. After that, our tests/
directory should look something like this:
Now that we have a location for our view file tests, we need to write one.
In our test class, we will create a Response
instance like the one at the end of our page script. We will pass into it the view file path and the needed variables. We will finally require the view, then check the output and headers to see if the view works correctly.
Given our articles.html.php
file, our initial test might look like this:
When we run this test, it will fail. We rejoice, because the $expect
value is empty, but the output should have a lot of content in it. This is the correct behavior. (If the test passes, something is probably wrong.)
In the above examples we paid attention only to output from echo
and print
. However, it is often the case that a page script will also set HTTP headers via header()
, setcookie()
, and setrawcookie()
. These, too, generate output.
Dealing with these output methods can be problematic. Whereas the Response
class uses output buffering
to capture echo
and print
into return values, there is no similar option for buffering calls to header()
and related functions. Because the output from these functions is not buffered, we cannot easily test to see what's going on.
For example, say we have some code like this in a contrived view file:
We can now test the Response
object to check the HTTP body as well as the HTTP headers.
Many times, a legacy application will have a view or template system already in place. If so, it may be sufficient to keep using the existing template system instead of introducing a new Response
class.
If we decide to keep an existing template system, the other steps in this chapter still apply. We need to move all of the template calls to a single location at the end of the page script, disentangling all of the template interactions from the rest of the business logic. We can then display the template at the end of the page script. For example:
For consistency's sake, we should either use the existing template system or wrap all template logic in view files via Response
objects. We should not use the template system in some page scripts and the Response
object in others. In later chapters, it will be important that we have a single way of interacting with the presentation layer in our page scripts.
Most of the time, our presentation is small enough that it can be buffered into memory by PHP until it is ready to send. However, sometimes our legacy application may need to send large amounts of data, such as a file that is tens or hundreds of megabytes.
At send()
time, the Response
will require the view file, which sets a header and the last call with arguments. The Response
then sends the headers and the captured output of the view (which in this case is nothing). Finally, it invokes the callable and arguments from setLastCall()
, which streams out the file.
In the example code from this chapter, we had only a handful of variables to pass to the presentation logic. Unfortunately, it is more likely that there will be 10 or 20 or more variables to pass. This is usually because the presentation is composed of several include
files, each of which needs its own variables.
Say we have a view file that includes a header.php
file, like this:
Our page script will have to pass $page_title
, $page_style
, and $site_nav
variables in order for the header to display properly. This is a relatively tame case; there could be many more variables than this.
We can then modify the header.php
file to use the HeaderDisplay object, and the page script can pass an instance of HeaderDisplay instead of all the separate header-related variables.
In the example code for this chapter, we concentrated on presentation logic in page scripts. However, it may be the case that domain classes or other support classes use echo
or header()
to generate output. Because output generation must be restricted to the presentation layer, we need to find a way to remove these calls without breaking our legacy application. Even classes that are intended for presentation purposes should not generate output on their own.
For example, say we have a class method that looks like this:
We can convert it to something like this instead (and remember to add escaping!):
When rearranging the page script to separate the business logic from the presentation logic, we may discover that the presentation code makes calls to Transactions or other classes or resources. This is a pernicious form of mixing concerns, since the presentation is dependent on the results of these calls.