whateverthing.com

Quick Web Apps with Composer and Silex, Part Two

In Part One, we built a basic "Hello Watson" web app using Composer and Silex. In this installment, we're going to add an upload form and a way to view the uploaded images.

Smooth Upload Action

First, we have to decide where the images belong. In my case, I created a folder called 'uploads/' at the same level as the bootstrap.php file - you can create it elsewhere, if you wish. Then, add the location to the application container in the bootstrap.php file - you'll end up with something that looks like this:

<?php

require __DIR__ . '/vendor/autoload.php';

$app = new Silex\Application();
$app['debug']=true;
$app['upload_folder']=__DIR__ . '/uploads';

The next stage is creating the form in web/index.php. As we are in an early stage of the project, we'll do this in the most horrible way imaginable: plain HTML in a string. (Sidenote: That should be an exclamation of frustration. "PLAIN HTML IN A STRING, BATMAN!"). However, because I'm not completely crazy, I'm going to use HEREDOC notation to make it more readable:

<?php

require __DIR__ . '/../bootstrap.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

$app->get( '/', function() {
    $upload_form = <<<EOF
<html>
<body>
<form enctype="multipart/form-data" action="" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="52428800" />
    Upload this file:
<br><br>
<input name="image" type="file" />
<br><br>
    <input type="submit" value="Send File" />
</form>
</body>
</html>
EOF;
    return $upload_form;
});

$app->run();

Note that we set the MAX_FILE_SIZE setting to 50MB - this is just for the web browser, though. There are two other places that file upload limits are controlled - the web server configuration and the PHP configuration. You'll have to adjust those settings to allow larger file uploads.

I should probably also describe what's going on in the action, because I didn't touch on that in Part One. Silex builds a "Route collection" of functions that you declare - these functions (sometimes called 'anonymous' or 'lambda' functions) can be passed around as a variable, so Silex puts them all together in a group and analyzes all incoming requests to see if they happen to match one of the declared routes. If there isn't a match, a NotFoundHttpException is thrown.

In order to process the upload on the server, we need to create a routing entry/action that can receive an HTTP "POST" request. It will be in charge of writing the upload to disk. Because this is just a test project, we're not too concerned about what the filename is, as long as it's unique.

Add this route to web/index.php, before the $app->run(); line:

$app->post('/', function( Request $request ) use ( $app ) {
    $file_bag = $request->files;

    if ( $file_bag->has('image') )
    {
        $image = $file_bag->get('image');
        $image->move(
            $app['upload_folder'], 
            tempnam($app['upload_folder'],'img_')
        );
    }

    // This is just temporary.
    // Replace with a RedirectResponse to Gallery
    return print_r( $request->files, true );
});

Note that I added the argument Request $request to the function. Silex detects this and dutifully passes the Request object in for you. The object contains all kinds of juicy info about the incoming request, but for our purposes, we only care about the 'image' field of the incoming form - that's where the data is.

The Viewer

We'll probably want to view the uploaded images, right? Because I'm mostly testing with the PHP internal dev server, rather than a full Apache server, I'm going to take an unusual approach to retrieving the images. We're going to implement a routing entry that accepts a filename identifier as a parameter and then returns a BinaryFileResponse object, instead of just linking to an image. This approach can be necessary in larger applications to enable access control restrictions on individual files.

Here's what the controller action could look like:

$app->get('/img/{name}', function( $name, Request $request ) use ( $app ) {
    if ( !file_exists( $app['upload_folder'] . '/' . $name ) )
    {
        throw new \Exception( 'File not found' );
    }

    $out = new BinaryFileResponse($app['upload_folder'] . '/' . $name );

    return $out;
});

This makes an attempt to verify that the file exists, but use caution: enterprising users could (in theory) use a function like this to output any file on your system. Any user is free to submit any data to your web application - it is always YOUR responsibility to sanitize or reject the input. I'll touch on ways to do that in a separate post.

Note that I added the argument {name} directly in the route name. Silex will interpret that and automatically put the value in the $name argument of the function.

Of course, now we need a way to tie the system together. Because the focus of this part is on the Upload behaviour, we'll gloss over the topics of thumbnail generation, database storage, and those fancy things. We're going to be brutally ugly, for the sake of experimentation and fun.

The "view" routing entry will loop through the contents of the upload folder and generate <img> tags for each item, passing the filename to the Viewer route we added in the last section.

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

    $out = '<html><body>';

    foreach( $images as $img )
    {
        $out .= '<img src="/upthing/web/index.php/img/' . basename($img) . '"><br><br>';
    }

    $out .= '</body></html>';

    return $out;
});

As you can see, this is another travesty of "HTML in a string" - we'll work on fixing that in the next part of the series. For now, you should be able to load http://localhost/ and upload images, and then visit http://localhost/view to view the "gallery".

That's all for Part Two - we've taken our "Hello Watson" app from a simple echo chamber to an interactive web application, albeit a very shoddy one. If you've been a developer for a while, you've probably seen code that's worse - and that's why Part Three will focus on refactoring and implementing some best practices.

Thanks for checking in! :)

Continue to Part 3 »
« Back to Part 1
View the Source on GitHub »

Published: June 30, 2013

Categories: howto

Tags: coding, development, dev, howto, quick-web-apps