A Silex example

Some weeks ago I was asked to develop a small custom web app for a client of mine.
The request was for a small and simple app but with a couple of gotchas.
Scenario is a real-estate agency willing to showcase a single product. They needed:
Access control on all of the pages
CRUD on users
Control over files being downloaded (no hardlinking)

My suggestion to the client has been to go for a Silex application since it uses well designed components coming from Symfony
The implementation doesn’t differ much from a basic template, but some steps were tricky since the official documentation is a bit lacky or hard to follow, omitting a couple of needed “require” here and there

I succeeded in satisfying customer’s requests and billed a total of 5 hours, including cretion of thumbnails, uploading of documents, extraction of the first page of PDFs as thumbnail ¹ ,  adapting to some changes in the initial requirements (never had a project that didn’t change requirements during development, not even is micro projects like this one).

This is the cleaned up log of what I did to get it working
First step has been the download of the standard Silex “fat” package which has then been uncompressed in the document root

composer.json
composer.lock
vendor/
 |-> autoload.php
 |-> composer/
 |-> doctrine/
 |-> monolog/
 |-> pimple/
 |-> silex/
 |-> swiftmailer/
 |-> symfony/
 L-> twig/
web/
 L->index.php

Of course I had to set up a .htaccess file to reroute all requests to web/index.php

    Options -MultiViews
    RewriteEngine On
    RewriteBase /web
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]

At this point I had to set-up the index.php file which in a silex application is the application itself
Here I’ll skip over some of the headaches the official documentation gave me due to “use” statements missing
The actual working top part of the index.php file is:

require_once __DIR__.'/../vendor/autoload.php'; 
require_once './UserProvider.php';
require_once './BinaryFileResponse.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Doctrine\DBAL\Schema\Table;
use Silex\Provider\FormServiceProvider;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\User;

As you see I include two classes here. One is an implementation of the UserProviderInterface as well explained in the documentation, so not much to add on that subject
The other is a class that Apparently is not shipped inside the fat tgz even though it’s referenced in the documentation regarding the related bundle (Symfony\Component\HttpFoundation)
When I discovered my install lacked that class I just went to the official github repo and copied/pasted it
I kept my copy outside the exploded silex directory tree to make updates easier should the need arise.

Even if this app was very small and simple there was no reason to over-simplfy and I’m a real fan of ORM so I used Doctrine even if in the silex context it only provides a DBAL

a few lines of code take care of db setup :

$app->register(new Silex\Provider\DoctrineServiceProvider(), array(
    'db.options' => array(
        'dbname' => 'mydb',
	'user' => 'myuser',
	'password' => 'mypass',
	'host' => 'myhost',
	'driver' => 'pdo_mysql',
    ),
));

$schema = $app['db']->getSchemaManager();
if (!$schema->tablesExist('users')) {
    $users = new Table('users');
    $users->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => true));
    $users->setPrimaryKey(array('id'));
    $users->addColumn('username', 'string', array('length' => 32));
    $users->addUniqueIndex(array('username'));
    $users->addColumn('password', 'string', array('length' => 255));
    $users->addColumn('roles', 'string', array('length' => 255));

    $schema->createTable($users);

    $app['db']->insert('users', array(
      'username' => 'fabien',
      'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
      'roles' => 'ROLE_USER'
    ));

    $app['db']->insert('users', array(
      'username' => 'admin',
      'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
      'roles' => 'ROLE_ADMIN'
    ));
}

Having taken care of the db setup we can proceed with the rest of the basic application setup:

$app['debug'] = false;
$app['security.firewalls'] = array(
    'login' => array(
        'pattern' => '^/login.html$',
    ),
    'secured' => array(
        'pattern' => '^.*$',
        'form' => array('login_path' => '/login.html', 'check_path' => '/login_check'),
        'logout' => array('logout_path' => '/logout.html'),
        'users' => $app->share(function () use ($app) {
		return new UserProvider($app['db']);
        }),
    ),
);

$app['security.access_rules'] = array(
    array('^/admin.html', 'ROLE_ADMIN'),
    array('^.*$', 'ROLE_USER'),
);

The app’debug’] value is used to control the output of stack traces in case of errors, of course you want to keep it to false when in production, not to reveal details on your application which may lead to weaknesses
Other than that you see I set up the firewall rules for the application since one of the main requests was a role based access control.
As you see I define a single page as reachable by everyone regardless of whether logged in or not (the login page). This is vital in order to let people log-in.
The only other firewall definition matches all other possible requests, this means: “If you’re not logged in I only show you the login page”
Please note that in defining firewalls order IS important since rules are matched in the order they are written. If I had put the login.html rule after the more comprehensive ^.*$ one it would have never been matched

Last but not least I added some RoleBasedAccessControll (poor man’s flavour) preventing non admin users to see the /admin.html page
You’ll see in the following methods I also used a more hardcoded approach: I was in a real hurry and this aspect would benefit from some refactoring
My personal taste in this matter would be to use annotations which I find very elegant and convenient since they stay very close to the code they annotate making modifications easier to write

Next step I took was the registering of the needed components for the application.
This happens after the definition of the firewalls to avoid a critical failure due to an undefined key

$app->register(new Silex\Provider\UrlGeneratorServiceProvider());
$app->register(new Silex\Provider\SecurityServiceProvider());
$app->register(new Silex\Provider\SessionServiceProvider());
$app->register(new FormServiceProvider());
$app->register(new Silex\Provider\ValidatorServiceProvider());
$app->register(new Silex\Provider\TranslationServiceProvider(), array(
    'translator.messages' => array(),
));

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

As you can see I also registered Twig and in the meantime also set up the base path to let it know where to find templates
I chose to keep my templates in a views/ folder inside the web/ one
Of course I also added a .htaccess in the views/ folder preventing anyone to retrieve files from inside it. A simple row did the trick:

deny from all

pretty ominous if you ask me 🙂
Letting people see unrendered templates is never a good idea

next come the controllers
The first one is the login one:

$app->get('/login.html', function (Request $request) use ($app) {
    return $app['twig']->render('login.html', array(
        'error'         => $app['security.last_error']($request),
        'last_username' => $app['session']->get('_security.last_username'),
    ));
});

Nothing major to note here.
The template code embeds the form since in this context we don’t really care about forgery
Form input names are the default ones Silex expects so no more mapping needed here

<form action="{{ path('login_check') }}" method="post">{{ error }}
 <h1>Autenticazione</h1>
 <input type="text" name="_username" value="" />
<input type="password" name="_password" value="" />
<input type="submit" value="Invia" />
</form>

An interesting controller is the one for adding users to the db:

$app->match('/admin.html/aggiungi', function (Request $request) use ($app) {
    if (!$app['security']->isGranted('ROLE_ADMIN')) {
     return $app->redirect('/login.html');
    }
    $errori = array();

    // some default data for when the form is displayed the first time
    $data = array(
        'name' => 'Inserire un nome',
        'password' => 'inserire una password',
        'admin' => false
    );

    $form = $app['form.factory']->createBuilder('form', $data)
        ->add('name')
        ->add('password')
        ->add('admin','checkbox',array(
	'label'     => 'È admin?',
	'required'  => false,
	))
        ->getForm();

    if ('POST' == $request->getMethod()) {
        $form->bind($request);

        if ($form->isValid()) {
	    $data = $form->getData();
	    $password=$app['security.encoder.digest']->encodePassword($data['password'],'');        
            $role = 'ROLE_USER';
            if($data['admin']==true){
		$role = 'ROLE_ADMIN';
            }
            try{
	    $app['db']->insert('users', array(
	    'username' => $data['name'],
	    'password' => $password,
	    'roles' => $role
	    )); 
	    return $app->redirect('/admin.html');
	    } catch(Exception $e){
		$errori[] = "Utente già presente";
	    }

        }
    }

    // display the form
    return $app['twig']->render('admin.html', array('form' => $form->createView(),'errori'=>$errori,'active_tab'=>'admin'));
});

Here you see the Form bundle in action taking care of the creation and validation of the form needed for creating users
Also you can see I added a checkbox to allow the creation of new administrator here the code is pretty self explanatory
If it’s a POST request and the form content is valid and the creation succeeds internal routing is invoked to reroute the user to the “/admin.html” page without providing a form

The admin template embeds a small set of IFs controlling which part of the template to render:

<h1>Pagina di amministrazione</h1><br/>
{% if errori is defined %}
 <div class="admin_errori_wrapper">
 {% for errore in errori %}
 {{errore}}
 {% endfor %}
 </div>
{% endif %}

{% if users is defined %}
 <h2>Utenti esistenti</h2>
 <ul>
 {% for user in users %}
 <li><a href="/admin.html/elimina/{{ user.username|e }}">Elimina</a>&nbsp;{{ user.username|e }}{% if user.roles.0 == "ROLE_ADMIN" %} (Admin) {% endif %}</li>
 {% endfor %}
 </ul> 
 {% endif %}
 <a href="/admin.html/aggiungi">Aggiungi</a>
{% if form is defined %} 
<form action="#" method="post"> 
 {{ form_widget(form) }}
 <input type="submit" name="submit" />
</form>
{% endif %}
</div>

Another request was to prevent hardlinking of actual content so I created a gallery page that lists content of a images folder and another controller rendering the actual image, thus giving me the ability to control who is getting the images. Of course the real images folder contains a .htaccess preventing direct access

The controllers are :

$app->get('/immagini.html',function() use ($app) {
 $immagini = array();

 $files =getSortedFiles(PATH_DOCUMENTI_IMMAGINI);

foreach($files as $file) {
 if ($file != ".htaccess" && $file != "" && $file != "." && $file != ".." && $file != "thumbnails" && !is_dir($file)) { 
 $immagine['file'] = $file;
 $immagine['thumb'] = str_replace('.pdf','.png',$file);
 $immagini[] = $immagine;
 }
 } 
 return $app['twig']->render('immagini.html',array('active_tab'=>'immagini','immagini'=>$immagini));
});

$app->get('/immagini.html/{nome}',function($nome) use ($app) {
 //return $app->sendFile();
 $file = PATH_DOCUMENTI_IMMAGINI.'/' . urldecode($nome);
 return new BinaryFileResponse($file, 200, array(), true, 'inline'); 
});

$app->get('/immagini.html/thumbnails/{nome}',function($nome) use ($app) {
 //return $app->sendFile();
 $file = PATH_DOCUMENTI_IMMAGINI.'/thumbnails/' . urldecode($nome);
 return new BinaryFileResponse($file, 200, array(), true, 'inline'); 
});

This way the client only needs to upload an image to the correct folder (and possibly a thumbnail as well) in order to see it listed in the gallery

The same approach has been followed for other documents

As noted earlier the copy of the silex_fat.tgz I started working with lacked the BinaryFileResponse class. I re-downloaded the tgz today and now the class is there, just keep your eyes open

Not bad for half a day’s job, right?


¹ Easily obtained with convert and a one-liner:

find -name "*pdf" | while read fn ; do convert -size 300x220 "${fn}[0]" "`basename "${fn}" ".pdf"`"".png"; done;

8 Responses to A Silex example

  1. Paula

    Hello, I am new to Silex and trying to understand how to create a working structure and application and go from there. Luckily, I found your page and I am wondering if you have the entire sample application up on Github? Your instructions here would help to walk through the code to understand it better. Kindly send me a response at my given email address. Thank you.

    • I didn’t set up a project on github, but I started from the standard silex documentation.
      You can start from there as well and easily retrace my steps

  2. does all of the above go into ones file, i.e. index.php?

    and thanks for the code/tutorial

    • Not necessarily all, but most of it yes
      By using a rewrite that redirects all requests to index.php you effectively create a front controller
      It’s the same approach most CMS and frameworks use

      If you need a more structured approach you may want to consider using a full Symfony2 stack

  3. Ciao Marco, complimenti per il sito, mi stavo documentando su Silex, perché avevo in mente di iniziare un piccolo progetto. Ti va di partecipare se hai tempo???

    • Ciao
      Sì, compatibilmente col tempo e con l’obiettivo del progetto volentieri
      Puoi contattarmi per mail a info chiocciola marcoalbarelli punto eu

  4. David

    Thanks Marco,
    Your example has given me a better idea of how to use Silex to get a website up and running. I found the Silex documentation far too terse and with huge assumptions about the level of expertise a new user may have. When the learning curve is steep a few examples are worth hundreds of pages of official documentation.

    • I completely agree on the state of Silex documentation. I am quite good at using Symfony, but I found the silex docs somewhat too “thin” myself
      Glad of having been helpful

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *


*