Welcome to the third installment of PHP-ing like a productive person. It’s code writing time. Here is how I like to start:
touch index.php
Yes, I’m a toucher. Sue me!
The Front Controller
The index file seems like a good place to start, right? That’s the first place your browser will hit. This is the central point of your application. And just for kicks, I will designate this file to be my front controller. What does that mean? It means that it will do all the routing. It will capture the GET requests from the visitors and then send them where they need to go.
One of the things I want in this pastebin incarnation are nice URL’s. This, is what I consider an ugly URL:
http://example.com/?paste=2345
On the other hand, this is what I consider a nice(er) URL:
http://example.com/2345
What’s the difference here? Well, the difference is in how Apache understand these two requests. The first one means “Yo, Apache, bring me index.php – I have a GET request for him”. The second means “Yo, Apache – I want to go to the 2345 folder and see what’s there!”
In other words, if you use the second URL Apache will try to find an index file in /vagrant/www/2345 directory. We don’t want that. We want our own application handle all the routing in hourse so to speak. How do we accomplish that?
We tell apache to screw off – that’s how. There are two ways to do this. In our vagrant server the easiest way to get it done is to use Apache ModRewrite. First, let’s enable it:
sudo a2enmod rewrite
Next let’s upgrade out site config:
sudo vi /etc/apache2/sites-available/project
In that file we need to add 5 new lines under the Directory heading. Everything under allow from all is new:
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
allow from all
# enable the rewrite engine
RewriteEngine On
RewriteBase /
# ignore file requests like /foo.php
RewriteCond %{REQUEST_FILENAME} !-f
# ignore directory requests like /foo/
RewriteCond %{REQUEST_FILENAME} !-d
# route stuff like /foo.php or /foo to index.php
RewriteRule . /index.php [L]
Or you can add the same lines in a .htaccess file in your project directory. The above method is considered a bit safer, but you do what you must.
Once you add these lines and restart Apache, all requests will be re-routed to index.php. Right now now that file is blank so there is nothing exciting going on yet. So let’s change that.
showPasteForm();
} elseif($uri == "/paste") {
$pasteCtrl->addNewPaste();
} elseif(PasteController::isValidPasteURI($uri)) {
$pasteCtrl->showPasteContents($uri);
} else {
$pasteCtrl->show404();
}
First line of the code includes Composer’s autoloader. This will allow me to seamlessly use the Twig and RedBean templates without actually having to explicitly include them anywhere. You’ll see this in action in just a minute.
Don’t worry about PasteController.php. It isn’t a thing yet. It does not exist. I just made it up. This code right will bug the fuck out if you try to access it via browser but that’s ok. What I’m doing here is sort of structuring the code in my head and putting it down. Having written all these calls to the non-existent methods gives me a pretty good idea what this new class ought to do:
- If the uri is / then it will show the blank form
- If the uri is /paste it will attempt to handle submitted data
- If it is something else, it will test if the URL is a valid paste number and show associated paste
- Otherwise, it will display an error
Let’s create the controller then!
Building the first class
I have a feeling you’re going to be sick of me touching things before this is done:
touch PasteController.php
I already know the basic layout of this class because I just drafted it out in my index class. All I have to do is to write it down now:
I added the the "hello world" line so that I can verify my code is working. I can do that by navigating to http://localhost:8080 on my host OS. If I nothing is amiss I should see the age-old one-line greeting echoed back to me.
Creating Twig Templates
Hello world is nice, but I want to make something real. In the first installment of this series I mentioned that echoing HTML from PHP scripts is not the greatest practice. So I want to do this right. I want to set up some Twig templates:
mkdir templates
touch templates/header.html
touch templates/form.html
touch templates/footer.html
Inside header.html I put... Well, header stuff:
{{ title }}
The {{ title }} bit is where we are going to pass the page title. In case you haven't noticed this is the Twig template syntax. The form.html file is equally simple:
{% include 'header.html' %}
Paste It
{% include 'footer.html' %}
Yep, I'm importing the header and footer files. Twig will assemble all of it for me into one nice file and I haven't used a single line of PHP in these templates. This is rather elegant, don't you think?
So, now lets twig it up in our controller:
// this is my twig instance
private $twig;
public function __construct()
{
// create twig environment and tell it where the templates live
$loader = new Twig_Loader_Filesystem('templates');
// set up a twig instance, enable auto-escaping
$this->twig = new Twig_Environment($loader, array('debug' => true,
'autoescape' => true,
'auto_reload' => true));
}
public function showPasteForm()
{
// load the form template
$template = $this->twig->loadTemplate('form.html');
// render and pass in the title at the same time
echo $template->render(array('title' => 'Paste It'));
}
Note, no include statements but Twig works! That's the magic of auto loading.
This is more or less all there is to Twig. It takes two lines to set it up, and another line or two to actually use it. There is really no reason not to include this library in your projects.
Testing
Seeing how we have our first class taking shape, it is probably a good idea to start building our first unit test:
mkdir tests
touch tests/PasteControllerTest.php
To make a test we extend the PHPUnit_Framework_TestCase class like this:
ctrl = new PasteController();
}
public function testIsValidPasteURI()
{
$temp = $this->ctrl->isValidPasteURI("/234");
$this->AssertTrue($temp);
$temp = $this->ctrl->isValidPasteURI("/foo");
$this->AssertFalse($temp);
$temp = $this->ctrl->isValidPasteURI("/foo/434");
$this->AssertFalse($temp);
$temp = $this->ctrl->isValidPasteURI("234");
$this->AssertFalse($temp);
}
}
We can run it by issuing the command:
phpunit UnitTest tests/PasteControllerTest.php
As you can see I'm testing the "easily testable" method isValidPasteURI. Why? Because it actually returns a value. These sort of methods are easy to unit test because you can just call them repeatedly with a wide variety of test values, and then make assertions about the results. Methods which produce side-effects and return no values (like showPasteForm) are little less testable.
The results should look a bit like this:
Why did this test fail? Well, we made our isValidPasteURI method always return true... My test expects it to return false with some values, and true with others. This was exactly what we should have expected. Now I can go back and fix the method so that it passes all my tests.
We know that a REQUEST_URI will need to start with a / and be followed by a number so here is a possible implementation:
/**
* Returns true if $uri contains a valid (numeric) paste id.
*
* A valud URI should looke like /234 - anything else is not valid
*
* @param string $uri a REQUEST_URI value that should be numeric
* @return boolean tru if URI is valud, false if it is not
*/
public static function isValidPasteURI($uri)
{
// if starts with /
if(strpos($uri, "/") === 0)
{
$tmp = substr($uri, 1);
if(is_numeric($tmp))
return true;
}
return false;
}
Is this correct? I don't know, let's see:
As far as my Unit Test is concerned, the method performs acceptably. Is my Unit Test correct? Is it exhaustive enough? Probably not, but I can add to it later if I feel extra vigilant.
How do we test methods in which the main functionality is producing side effects? Good question. It seems that we should at least make an effort to test our showPasteForm which renders a Twig template and sends it to the browser. How do we make assertions about output though?
Well, if you just call the method in your test and run phpunit with --verbose --debug attributes, it will dump the raw HTML to the console and you can eyeball it. But that's not really testing - that's checking. We want to test! Sadly, checking output is not really what Unit Testing was designed for.
To test this function we would ideally want to emulate what the browser is doing - so generate a fake request to index.php and see if a form is displayed by patterm matching the HTML. That's not unit testing though. That's acceptance testing, and there are actually frameworks that let us do that - like Codeception for example. But I want to concentrate on unit tests now, which test elements of your code in isolation.
Fortunately PHPUnit has some functionality that can help us test this method. There is a near little assertion named expectOutputRegex. As you can probably imagine it will do some pattern matching on the output. This is how you use it:
public function testShowPasteForm()
{
$this->expectOutputRegex("/Paste It<\/title>/");
$this->ctrl->showPasteForm();
}
This is little backwards from all other tests because you assert firsts and run code afterwards, but it works. The downside of this is that you can usually only make one assertion per test. If you assert multiple things, only the last one will actually be tested. So you better pick a good regex.
What I want to know when this test runs is that Twig assembled my templates correctly. If you remember, in the header template the title was not defined. It contained a variable. In addition the form template did not have a title tag - it imported it. So the only way for the rendered page to have a title, and a correct one is for Twig to have worked correctly. Hence, that's what I'm pattern matching in this test.
Error Handling
Let's handle that pesky error situation when someone inadvertently tries to access a URI that is not a valid paste address. For example navigating to /foo should produce a 404 error code so that browsers know that nothing is there. We already have a method for that in our PasteController but... Well, is that a good place for it? Why a controller related to paste stuff should handle routing errors?
How about we make an ErrorController.php class and encapsulate that functionality away:
This works, but it doesn't really display any meaningful message to the user. The browser knows there was a 404 error, but the user sees a blank page. It would be better if we could pull in our templating engine and display a nicely formatted error message.
But... Well, this is not really an issue but I already initialized Twig and set it up to my liking in PasteController. Now I will have to do it again. And if I define another controller, I will have to do it there too. I know it's only two lines but that's not the case. I'm worried about the configuration options (like enabling the auto-escaping feature). I want it to be consistent throughout my application.
So let me make a TwigFactory! It will be a static class that will give you and instance of Twig environment pre-configured to your liking. Both Paste and Error classes will be able to grab it whenever they need it:
true,
'autoescape' => true,
'auto_reload' => true));
}
}
Refactoring
My project directory is getting messy. I have controller classes floating around, I have helper classes (like the TwigFactory) and all kinds of other stuff in there. I think it's time to do some cleanup. It is usually a good idea to commit your existing code right before you're going to make major changes:
git add .
git commit
This way if we royally mess something up, we can quickly roll back to the last known good state for the project and start over. Now we can proceed with potentially destructive refactoring actions:
mkdir controllers
mkdir helpers
mv *Controller.php controllers/
mv *Helper helpers/
This should actually make our working environment much cleaner. Observe:
Now that I cleaned everything up I will need to go back and correct the include statements everywhere and then re-test everything. Which reminds me, we should build a unit test for Error Controller as well. Good news is that we can crib most of it from the Paste Controller test:
err = new ErrorController();
}
public function testObjectInitialization()
{
$this->assertObjectHasAttribute("twig", $this->err);
}
public function testShowPasteForm()
{
$this->expectOutputRegex("/404<\/h1>/");
$this->err->show404();
}
}
Now that I have multiple tests I can actually run them together by simply pointing PHPUnit at my test directory like this:
Auto Loading and Code Standards
I just realized something... I've been doing this wrong. I'm kinda blogging along as I assemble this code, and it suddenly struck me that I am not using one of the great features that comes with Composer - auto-loading. Or rather, I'm using it for all the third party classes, but I still have to "require" my own classes. This is silly.
But alas, to actually leverage this feature my code should conform to PSR-0... Which it does not. It is not properly namespaced, and not organized the way it should be. So we need to do even more refactoring. Best do it now, while the code-base is still small and uncomplicated.
The way Composer's autoloading works is that you can specify your own namespace in the composer.json like this:
"autoload": {
"psr-0": {
"SillyPastebin": "src/"
}
}
Here I'm telling composer to look for SillyPastebin code in the src/ directory. This directory does not exist yet. I will have to create it.
Once I have this directory in place, the rest is just a pattern matching game. When you instantiate a namespaced class like this:
SillyPastebin\Namespace\SomeClass
the composer autoloader simply converts all the php namespace delimiters (these things: \) into system path delimiters (in our case /) using the directory you specified in the config file as root. So if it sees the invocation I shown above it will attempt to auto-load a class located at:
/src/SillyPastebin/Namespace/SomeClass.php
This is not how my code is organized. I have controller and helper folders in my top level directory and nothing is namespaced. With the code as it is, there is simply no way for me to leverage this great feature. Which means we have some housekeeping and refactoring to do.
First let's move the files around to conform to these conventions:
mkdir src
mkdir src/SillyPastebin
mv controllers/ src/SillyPastebin/Controller
mv helpers/ src/SillyPastebin/Helper
When finished, my directory structure will look like this:
Next let's refactor our code:
twig = \SillyPastebin\Helper\TwigFactory::getTwig();
}
And:
twig = \SillyPastebin\Helper\TwigFactory::getTwig();
}
And:
true,
'autoescape' => true,
'auto_reload' => true));
}
}
Note that I had to prefix the Twig classes with \ to indicate they are not part of the SillyPastebin\Helper namespace.
Finally here is how you change the index:
We'll also need to refactor our tests the same way, but I'm not gonna show that because it's basically more of the same. You remove the require statements, and you add SillyPastebin\Namespace in front of all the class instatioation calls and that's about it.
Last thing to do is to update our composer config to make sure the auto-loading feature works:
composer update
The whole thing took about five minutes, and I achieved something really cool - I will never, ever have to write another require or include statement for this project. The first line of index.php is actually the only include that I will ever need from now on. This is very, very cool and I get this for free with Composer as long as I namespace my code the right way... Which is what I should be doing anyway.
Lessons Learned
I started this series with the best intentions - I wanted to write this thing right. I wanted to practice what I preached in Part 1, and yet when I sat down and started writing code bad habits started sneaking in. This happens all the time - you forget, or you willingly ignore some rule or best practice. At first it works out fine, but then as you continue working things start to crumble. The code starts looking subtly wrong.
You know what reeled me back onto the correct path again? Unit tests. No seriously, scroll back a few paragraphs and look at the code I posted for ErrorControllerTest class. I saw these lines piling up on top:
require_once "controllers/ErrorController.php";
require_once "helpers/TwigFactory.php";
Here is what I realized: I may have more helper classes in the future. Also Models, which we didn't even talk about yet. I will have to include every single one of them, on every single unit test. That's silly. If I had to do this once in my index file, I'd probably be fine with it - but now I saw dozens of unit tests, with long, long lines of includes on top, and it looked wrong.
And so, I ended up refactoring my code to conform to the PSR-0 standard, and to use proper name spacing. Unit testing saved the day once again, and not by detecting errors, but by forcing me to think about how I structure my code. I mentioned this in part one - that's the hidden value of Unit Tests. They make you think not only about what your code is doing, but also how it is doing it.
This is the primary reason why I didn't go back and rework this post to look as if I meant to do it this way all along. It would probably be clearer, but I wanted to showcase how even if you have best intentions you can get easily lulled into complacency and how following some best practices and using the right tools can jolt you right back onto the correct path.
Next time I'll try to finally get some database interaction accomplished using RedBean. Unless I get side-tracked again.
Related Posts
I’ve still been running along in parallel with my own implementation in Elisp. It’s been a lot of fun! It’s got syntax highlighting, diffs, and supports three different backend databases. Again, here’s a demo hosted for the short-term, by same text editor instance I used to write it. For visitors from the future, here’s a screenshot (and those fuzzy timestamps update live!). Try clicking the “diff” link.
http://zeus.nullprogram.com/pastebin/vQ5Y
I kept the server side as simple as possible. Counting only one backend database, it’s about 150 lines of code and only serves one static page, a few scripts, and a single JSON form. All the heavy work is done client-side, including page generation, syntax highlighting, and unified diffs. The downside is that I bet it’s not very search-engine friendly. I do have some unit tests in place, since you’re using unit tests for your pastebin.
Thanks, this has been very educational.
Pingback: Unit Testing Sinatra Apps | Terminally Incoherent