whateverthing.com

Quick Web Apps with Composer and Silex, Part Three

In Part One and Part Two, we built a small web application with a little bit of interactivity. However, it has some problems - it might run fine for a little while, but eventually things will require maintenance. Someone may want to pretty it up, for example, or you might want to add a feature like thumbnails. After being away from the code for six months or a year, you might come back to it and wonder "WHAT was I thinking?!"

This begins a very common sequence in our field: confusion, denial, anger, refactoring, and acceptance testing - the five stages of code grief.

Let's focus on refactoring for now. Specifically, the kind of refactoring you do when a crazy request comes in with a tight deadline. In the best-case scenario, a codebase has a unit test harness that lets you refactor at will and automatically yells at you if you break something. Like most legacy codebases, UpThing is not a best-case scenario. We're going to have to refactor by the seat of our pants.

Twig: It's Wet-Your-Pants Awesome

You probably noticed that I dislike putting HTML in a string. This is something I picked up after building a 10K+ line hobby codebase that was full of rambling functions and hard-coded HTML strings. There were also a few memorable workplace encounters, such as form generator systems.

Templating engines are a great solution to this. Many people argue that since PHP started out as a kind of templating system, that it suits the job just fine. They're not wrong, but it takes a lot of self-discipline to keep advanced logic where it belongs: in the code, not the template.

I use Twig. Twig is similar to Jinja and other templating systems. It has some advanced features, but at its core it is an uncomplicated baseline that you can extend and improve as necessary. Most importantly, it makes it easy for you to separate your business code from your display code - this makes maintenance and changes MUCH easier, and essentially allows you to refactor your code to be more straightforward and readable.

Let's Twigify UpThing

First, we need to add Twig to our project. On the command line, inside the UpThing folder, run this command:

$> composer require twig/twig

It'll ask for a version - for testing, I used 1.*, but the Silex documentation recommends ">=1.8,<2.0-dev". Either way, this will automatically add Twig to your composer.json file and download the code into the vendor/ folder.

Next, we have to initialize the Twig system in bootstrap.php. Silex has a Twig Service Provider that makes this easier than doing it by hand:

$app->register(new Silex\Provider\TwigServiceProvider(), array(
    'twig.path' => __DIR__ . '/views',
));

Enter the Templates

Now we can create our default layout in views/layout.html.twig:

<html>
<head><title>{% block sub_title '' %}{% block title 'UpThing' %}</title></head>
<body>
  <h1>UpThing</h1>
  {% block body %}
    Welcome to UpThing!
  {% endblock %}
</body>
</html>

While we're editing templates, lets also create upload_form.html.twig:

{% extends 'layout.html.twig' %}

{% block sub_title 'Upload an Image - ' %}
{% block body %}
<h2>Upload an Image</h2>
<form enctype="multipart/form-data" action="" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="52428800" />
<br>
<input name="image" type="file" />
<br><br>
    <input type="submit" value="Send File" />
</form>
{% endblock body %}

The "extends" line at the top is important, because it shows that we can now inherit the visual appearance of our site just like a class would inherit from a parent; in fact, that's exactly what Twig does behind the scenes. All the twig template code is parsed down into actual PHP classes, which enables you to enable server optimizations like "opcode caching" using APC.

Witness the Power of a Fully Operational Templating System

Now let's update the upload route to render the upload form. You can strip everything out of the original upload form route and replace it with this:

$app->get( '/', function() use ( $app ) {
    return $app['twig']->render('upload_form.html.twig');
});

Because there's no business logic in the form rendering, we've been able to offload it entirely to twig, which has cleaned up web/index.php and clearly indicated that the form is HTML and not code. This also has a side-benefit: now you can give the template file to a designer, and they can edit it directly instead of having to make you manually apply all of their changes.

Let's go ahead and do the same thing to the Gallery route. Create views/gallery.html.twig like so:

{% extends 'layout.html.twig' %}

{% block title 'Image Gallery - ' %}

{% block body %}
    <h2>Image Gallery</h2>
    {% for item in images %}
        <img src="{{ app.request.baseUrl }}/img/{{ item }}"><br>
    {% else %}
        <p><strong>Sorry, no images were found.</strong></p>
    {% endfor %}
    <br><br>
    <a href="{{ app.request.baseUrl }}/">Upload some images &raquo;</a>
{% endblock %}

Again you see that only the pertinent information is included, none of the layout/unimportant HTML. Now we just have to update the gallery route:

$app->get('/view', function() use ( $app ) {
    $image_glob = glob($app['upload_folder'] . '/img*');

    $images = array_map( 
        function($val) { return basename( $val ); }, 
        $image_glob 
    );

    return $app['twig']->render('gallery.html.twig',array(
        'images' => $images,
    ));
});

So that's how to upgrade a legacy product to Twig using Silex. If Silex is too heavyweight for your legacy system, the Pimple dependency injection container is much easier to integrate with older code, and you can use it to move toward Twig and other technologies.

Imagine the Possibilities

Imagine is a very useful tool for working with images. We're going to use it to modify our "/img/{name}" route to generate thumbnails on the fly.

Use "composer show imagine/imagine" to see some info about the package. When I ran it, it looked like the newest stable version was 0.4.1, so we'll use that in our composer.json:

$> composer require imagine/imagine 0.4.*

Since we're kind of lazy, we'll also use a Silex Service Provider that shows up on Packagist:

$> composer require neutron/silex-imagine-provider 0.1.*

Now we add this to bootstrap.php:

$app->register(new Neutron\Silex\Provider\ImagineServiceProvider());

If your PHP installation is configured with either Gd or Imagick support, you'll be good to go. Otherwise you'll have to get one of those libraries installed, but once that's done, we can begin modifying the image retrieval route to resize images on the fly.

Start by changing the route definition from '/img/{name}' to '/img/{name}/{size}'. Don't forget to add the $size parameter to the arguments of the anonymous function.

Because we don't necessarily want the old links to be broken, we should set a default for 'size'. We can do that by adding a call to value(), like so:

$app->get('/img/{name}/{size}', function(....) {
    .....
})
->value('size', 'small');

Here's how the final thumbnail generator will look:

$app->get('/img/{name}/{size}', function( $name, $size, Request $request ) use ( $app ) {
    $prefix = $app['upload_folder'].'/';
    $full_name = $prefix . $name;

    $thumb_name = '';
    $thumb_width = 320;
    $thumb_height = 240;

    if ( !file_exists( $full_name ) )
    {
        throw new \Exception( 'File not found' );
    }

    switch ( $size )
    {
    default:
    case 'small':
        $thumb_name = $prefix . 'small_' . $name . '.jpg';
        $thumb_width = 320;
        $thumb_height = 240;
        break;

    case 'medium':
        $thumb_name = $prefix . 'medium_' . $name . '.jpg';
        $thumb_width = 1024;
        $thumb_height = 768;
        break;
    }

    $out = null;

    if ( 'original' == $size )
    {
        $out = new BinaryFileResponse($full_name);
    }
    else
    {
        if ( !file_exists( $thumb_name ) )
        {
            $app['imagine']->open($full_name)
                ->thumbnail(
                    new Imagine\Image\Box($thumb_width,$thumb_height), 
                    Imagine\Image\ImageInterface::THUMBNAIL_INSET)
                ->save($thumb_name);
        }

        $out = new BinaryFileResponse($thumb_name);
    }

    return $out;
})
->value('size', 'small');

It's pretty long, and some of its functionality should likely be broken out into its own class, but that can be done later. In fact, that level of refactoring is where unit tests are most useful, so it might make more sense to find a way to test the action automatically first before refactoring it. For now, this change enables you to update the gallery to use "medium" or "original" images for popups/download links.

I had hoped to cover more ground in this part of the series, but that will have to wait for future installments. If anyone's interested in seeing the source code as a whole, use the contact address in the sidebar or ask me on Twitter. I haven't added it to Github yet, but I might get around to that at some point, if there's enough interest. :)

In memory of the dearly departed Google Reader, please remember to add Whateverthing.com's RSS feed to your feed reader.

Thanks for checking in!

Continue to Part 4 (Bootstrapping) »
« Back to Part 2
View the Source on GitHub »


Edits:
  • Fixed a bug in the final code block that was pointed out by steffkes on freenode's $silex-php IRC channel. "Original" requests would not have worked, because the instantiated object would never be returned.
  • Switched to using array_map for building the image glob, also suggested by steffkes. Thanks, steffkes! :) Note that this also involved changing the gallery template to output item instead of item.name
  • Fixed a few typos in the source examples that I discovered while working on Part 4.

Published: July 1, 2013

Categories: howto

Tags: coding, development, dev, howto, quick-web-apps, composer, twig, imagine, pimple