PHP Like a Pro: Part 4

In the previous installment we built a semi-working skeleton of our application. It is time to finally get down to the business and make it talk to the database and actually save and retrieve out content. Normally this would be the place when you buckle down, whip out your E/R diagram and start writing SQL queries to build your database. But, we are going to be using RedBean ORM for this project.

What does that mean? That means the amount of actual SQL we will need to write is very, very small. In fact, I believe I won’t need to write a single line of SQL until part five of this series. So instead of writing two thousand words about database modeling, I’m going to jump straight in and write some tests for my yet undefined method addNewPaste(). They are going to look like this:

    public function testAddNewPasteWithValidString()
    {
        $content = "some text to be pasted";
        $temp = $this->ctrl->addNewPaste($content);
        $this->assertNotNull($temp);
        $this->assertInternalType('integer', $temp);
    }

    public function testAddNewPasteWithNull()
    {
        $content = null;
        $temp = $this->ctrl->addNewPaste($content);
        $this->assertNull($temp);
    }

My first test attempts to add a new record with actual string content and expects an integer (insertion ID) in return. The second test sends in null as an argument and expects a null back. In other words we want our application to ignore null input, and return a null instead of an integer.

Here is the implementation – pay close attention because ReadBean is going to blow your mind:

use RedBean_Facade as R;

    public function addNewPaste($content)
    {
        R::setup();
        $paste = R::dispense("paste");
       
        $paste->content = $content;

        return R::store($paste);
    }

Yes, this code is actually going to work. If you never seen RedBean at work this probably bears some explaining.

The first line, binds RedBean_Facade to R. Why? Because all of ReadBean documentation uses R notation, but R is not actually a real class. If you download RedBean from the official website you get a single file which is essentially the whole package, compressed and minified, with the generated R class acting as the user interface. When you install RedBean via Composer you don’t bet the R class so you have to add this one line. It is a quirk of the framework, and you just have to remember to do it.

The setup line connects to the database. The dispense line gives you an interactive object/model – RedBean calls these objects “beans”. You manipulate the bean, and then you store it. The database schema is automatically altered to fit the data you are inserting (though there is an option to freeze the schema when you are ready to deploy into production).

Note that there is no SQL here. All is done at a very high level, and RedBean does sanitize your input. Granted, there is no reason why we shouldn’t pre-sanitize it just in case, but we can get to that later. Right now I just want to have some working code here.

You might be asking yourself, how does RedBean know about our MySQL database? Well, right now, it does not know about it. Right now it is using SQLite which is it’s goto database environment.

By the way, if the above code blows up on you saying you’re missing some drivers you’ll probably need to install sqlite support for php:

sudo aptititude install sqlite php5-sqlite php5-mysql

By default, RedBean stores the SQLite database file in /tmp/red.db. This is great for testing because we can easily wipe this database after each test. In fact, I added the following to my PasteControllerTest just to ensure that every unit test starts with a clean DB:

    public static function tearDownAfterClass()
    {
        if(file_exists("/tmp/red.db"))
            unlink("/tmp/red.db");
    }

Once we are done with the development phase and we are ready to have a more permanent storage we will simply need to modify the setup line, passing mysql hostname, username and password as arguments.

This is sort of the beauty of RedBean – it is absolutely hassle free. I can start building a complex application without actually worrying much about my database. Then once I have it done, I can simply connect it to MySQL, Postgess or whatever else the framework supports.

Let’s run our test now, shall we:

RedBean Test

RedBean Test

Oops, failed. Why? Well, RedBean is happily inserting null values into the database. This is a little bit of a tradeoff – if I defined my database up front, I would have a database error here. Since I didn’t RedBean is assuming I want null values in my DB (a completely valid assumption). In this case, data validation is on me.

Here is something to ponder: is returning null really the best way to handle this situation? Technically, null or empty pastes should not happen so perhaps it is better to throw an exception with an appropriate message?

I’m thinking about clarity and maintainability here. If someone else decides to use my method and it unexpectedly returns null, it may cause unexpected failures down the road. Null value can pass through layers of logic, and bubble up to somewhere else entirely where it blows the application up. If the method throws an exception it fails immediately with a clear and explicit error message.

Granted, if the null return value is well documented a programmer should be able to handle it. In fact, handling an null return value is less hassle than handling an exception. So this is a rather complex idea. Both approaches have their benefits and downsides.

I guess for us it is better to err on the side of caution, fail fast, and fail early. So let’s rewrite our test to assert exception rather than a null value:

    /**
     * @expectedException        InvalidArgumentException
     */
    public function testAddNewPasteWithNull()
    {
        $content = null;
        $temp = $this->ctrl->addNewPaste($content);
    }

Here is the corrected code with some data validation:

    /**
     * Adds new paste to the database.
     *
     * @param string $content a new paste content
     * @throws InvalidArgumentException if $content is null or empty
     * @return integer valid pasteID (insertion id) 
     */
    public function addNewPaste($content)
    {
        if(empty($content))
            throw new \InvalidArgumentException("Paste content cannot be empty or null");
        else
        {
            R::setup();
            $paste = R::dispense("paste");

            $paste->content = $content;

            return R::store($paste);
        }
    }

We can run the tests again and observe that valid entries are inserted to the DB, while the null or empty ones throw an exception as they should.

Let’s write a few tests for the showPasteContent method now:

    public function testShowPasteContentWith1()
    {
        $this->expectOutputRegex("/some text to be pasted/");
        $this->ctrl->showPasteContent("/1");
    }

    /**
     * @expectedException        InvalidArgumentException
     * @expectedExceptionMessage Invalid paste ID
     */
    public function testShowPasteContentWithInvalidUri()
    {
        $this->ctrl->showPasteContent("foo");
    }

    public function testShowPasteContentWith9999()
    {
        $this->expectOutputRegex("/No such paste/");
        $this->ctrl->showPasteContent("/9999");
    }

First test tries to render a paste that ought to be there already (we have inserted it in the previous test). The second test tries to use an invalid URI and expects an exception. The third one picks an ID I do not expect to exist in the database and expects to see an error message.

We haven’t made a Twig template for displaying the pastes yet so here it is:

{% include 'header.html' %}

    

PASTE #{{ pasteID }}

{{ content }}

Raw Content

new paste

{% include 'footer.html' %}

Here is the actual implementation:

    public function showPasteContent($uri)
    {
        if(!$this->isValidPasteURI($uri))
            throw new \InvalidArgumentException("Invalid paste ID");
        else
        {
            $pasteID = substr($uri, 1);
            R::setup();
            $paste = R::load("paste", $pasteID);

            $template = $this->twig->loadTemplate('show.html');
            echo $template->render(array('pasteID' => $pasteID,
                                            'content' => $paste->content));

        }

As you can see, fetching the data from the DB using RedBean is equally easy. You issue a load command, and you get a bean back. Then you can access the bean’s public fields to get at the data.

Let’s run another quick test:

PHPUnit test

PHPUnit test

Whoops. It seems that RedBean will return back a bean for non-existent entries without making much of a fuss. Once again we have to add some validation code.

But firs,t lets make a new template to handle the condition when the requested paste is not in the database:

{% include 'header.html' %}

    

No such paste

Sorry, paste with id #{{ pasteID }} does not exist or has been removed.

new paste

{% include 'footer.html' %}

Let’s fix our PHP code to actually handle the condition:

    /**
     * Renders a paste as a web page.
     *
     * @param string $uri A valid URI with a paste address
     * @throws InvalidArgumentException if the URI is invalid
     */
    public function showPasteContent($uri)
    {
        if(!$this->isValidPasteURI($uri))
            throw new \InvalidArgumentException("Invalid paste ID");
        else
        {
            $pasteID = substr($uri, 1);
            R::setup();
            $paste = R::load("paste", $pasteID);

            if(empty($paste->content))
            {
                echo $this->twig->render("nosuchpaste.html", array('pasteID' => $pasteID));
            }
            else
            {
                echo $this->twig->render("show.html", array('pasteID' => $pasteID,
                                                            'content' => $paste->content));
            }

        }

    }

You have probably noticed that I used the shorter, more succinct version of the Twig render method in the code above. This is perfectly valid, and actually more convenient in this particular case where we need to render two different templates based on a conditional.

One more quick unit test to verify we haven’t messed it up:

This time we pass with flying colors

This time we pass with flying colors

Up until this moment, I have been mostly relying on unit testing to make sure my code works. This is actually an excellent practice because it lets you concentrate on particular area of your code, and make sure it works in isolation before you integrate it with the rest of the code.

I wanted to take this opportunity to talk about the rather interesting integration testing framework called Codeception but unfortunately it appears that they’ve done broke themselves:

I swear, it wasn't me

I swear, it wasn’t me

So, I guess we’ll push the the integration testing discussion and examples until next installment. Unfortunately this means we have to test our application like ordinary chumps: using a web browser and actually typing crap in.

The test procedure ought to be something like this:

  1. Navigate to /
  2. Type in some garbage, hit submit
  3. Follow the link on the page to see the paste on it’s own page
  4. Hit some random URI to see “no such paste” message
  5. Hit /paste manually to see an error message

Step 3 reveals I haven’t actually implemented the “thanks for posting” style message. When a paste is submitted, all the user gets is a blank page. Let’s fix that.

We’re going to create a new method in PasteController and call it showThankYou(). What is it going to do? Among other things I want it to include a phrase like “Your paste is available here [LINK]”.

So here is a test:

    public function testShowThankYouWithValidContent()
    {
        $content = "some content";
        $temp = $this->ctrl->addNewPaste($content);
        
        $this->expectOutputRegex("/Your paste is available here: #$temp<\/a>/");     
        $this->ctrl->showThankYou($temp);
    }

Here is a template:

{% include 'header.html' %}

    

Thank You

Your paste is available here: #{{ pasteID }}

{% include 'footer.html' %}

And here is the implementation:

    /**
     * Renders a thank-you message with a link to a newly created paste
     *
     * @param integer $pasteID - a valid paste ID
     */
    public function showThankYou($pasteID)
    {
        echo $this->twig->render("thanks.html", array('pasteID' => $pasteID));
    }

Granted, this won’t do anything until we modify our index file:

if($uri == '/') {
    $pasteCtrl->showPasteForm();
} elseif($uri == "/paste") {
    $pasteID = $pasteCtrl->addNewPaste($_POST["content"]);
    $pasteCtrl->showThankYou($pasteID);
} elseif($pasteCtrl->isValidPasteURI($uri)) {
    $pasteCtrl->showPasteContent($uri);
} else {
  $errorCtrl->show404();
}

I also noticed that step 5 blows the application up. There is no code in my application to handle the situation when someone accesses the post url directly. To fix that we need four things. First, lets write some tests to define how our error is going to behave:

    public function testShowGenericErrorWithValidMessage()
    {
        $this->expectOutputRegex("/my test message/");
        $this->err->showGenericError("my test message");
    }

    public function testShowGenericErrorWithNull()
    {
        $this->expectOutputRegex("/Unexpected error./");
        $this->err->showGenericError(null);
    }

Note the second test – I bet that if I wasn’t starting off by writing unit tests, I would completely skip this condition because there is just no easy way to induce it simply by using the web interface.

Next we need the template:

{% include 'header.html' %}

Error

{{ errorMessage }}

{% include 'footer.html' %}

Finally, the implementation which goes in my front controller block:

if($uri == '/') {
    $pasteCtrl->showPasteForm();
} elseif($uri == "/paste") {
    try
    {
        if(!empty($_POST["content"]))
        {
            $pasteID = $pasteCtrl->addNewPaste($_POST["content"]);
            $pasteCtrl->showThankYou($pasteID);
        }
        else
            $errorCtrl->showGenericError("Paste content cannot be empty.");
    }
    catch (InvalidArgumentException $e)
    {
        $errorCtrl->showGenericError($e->getMessage());
    }
} elseif($pasteCtrl->isValidPasteURI($uri)) {
    $pasteCtrl->showPasteContent($uri);
} else {
  $errorCtrl->show404();
}

Our application is almost complete. Now we just need to add some finishing touches – like actually connecting our application to MySQL database, and freezing the DB schema.

In the first installment of this series I talked about the MVC pattern a lot, but in the end we haven’t really implemented any models. That’s mostly because we opted for RedBean which aims to make things simple and reduce the amount of typing you must do. If we went with Droctrine models would be among some of the first things we would have to build.

Still, RedBean does support the concept of models. We can create objects that will be auto-matically connected to beans and which we can use to do some extra validation, error checking and sanitization on our values. I want to accomplish that in the next part.

I also really want to try bringing in Codeception or similar acceptance testing framework and try to automate these “browse to the webpage and mess with it” tests. Perhaps when I sit down to write the next installment their website will be back.

Finally, a pastebin without code highlighting is kinda weak. So I want to see if I could pull in a third party syntax highlighting package like GeSHi and make it play nicely with our code.

This entry was posted in Uncategorized. Bookmark the permalink.



3 Responses to PHP Like a Pro: Part 4

  1. Grzechooo POLAND Opera Windows says:

    Codeception site is up now.

    Reply  |  Quote
  2. Luke Maciak UNITED STATES Google Chrome Linux Terminalist says:

    @ Grzechooo:

    Yep, it came back about three hours after I finished this post, and was already drafting part 5. I didn’t feel like going back and revising this one, so I pushed the Codeception section till the next installment.

    Reply  |  Quote
  3. davert Opera Windows says:

    @ Grzechooo:
    Yeah, we are hosted at GitHub pages and GitHub had some issues with availability these days.

    Reply  |  Quote

Leave a Reply

Your email address will not be published. Required fields are marked *