Cómo organizar bien un proyecto Silex

8 de noviembre de 2013

Silex es un microframework para crear sitios y aplicaciones web con PHP. Si desarrollas tus aplicaciones a mano y no quieres todavía dar el salto a un framework grande como Symfony, entonces Silex puede ser una buena solución.

Este tutorial supone que ya tienes conocimientos básicos de Silex (para ello, puedes leer el manual oficial de Silex) y que has creado una aplicación Silex siguiendo la estructura habitual proporcionada por el Silex-Skeleton.

Además, para simplificar los ejemplos de código, se va a suponer que la aplicación está formada solamente por las siguientes cinco rutas:

Nombre URL Descripción
portada / Portada del sitio
eventos_index /eventos/ Portada de la sección de eventos
eventos_show /eventos/{slug}/ Página de un evento específico
foro_index /foro/ Portada de la sección del foro
foro_show /foro/{id}/{slug}/ Página de una discusión del foro

La situación inicial

Siguiendo el modelo tradicional de programación de aplicaciones con Silex, todo el sitio web se puede crear en un único archivo PHP llamado index.php:

// archivo index.php
require_once __DIR__.'/../vendor/autoload.php';

$app = new Silex\Application();

$app->get('/eventos/{slug}/', function($slug) use($app) {
    ...
})->bind('eventos_show');

$app->get('/eventos/', function() use($app) {
    ...
})->bind('eventos_index');

$app->get('/foro/{id}/{slug}/', function($id, $slug) use($app) {
    ...
})->bind('foro_show');

$app->get('/foro/', function() use($app) {
    ...
})->bind('foro_index');

$app->get('/', function() use($app) {
    ...
})->bind('portada');

$app->run();

A finales de 2012 decidimos rehacer desde cero el sitio librosweb.es y optamos por desarrollarlo con Silex. Como al principio no teníamos tantas secciones como ahora, optamos por seguir esta forma de organizar el código e incluimos todas las rutas y controladores en un único archivo.

Como era previsible, este modelo no escala bien en cuanto el sitio es de una complejidad media. Al añadir todas las secciones del sitio, este archivo se convirtió en un monstruo inmanejable de miles de líneas de código. Así que en este tutorial te contamos los tres métodos alternativos que hemos utilizado para organizar bien el código de una aplicación Silex mediana y las ventajas e inconvenientes de cada una.

Método 1: Colecciones de controladores

El primer método alternativo que utilizamos se basa en las colecciones de controladores. El siguiente ejemplo muestra el nuevo código de la aplicación:

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

$app = new Silex\Application();

$eventos = $app['controllers_factory'];

$eventos->get('/', function () use($app) {
    ...
})->bind('eventos_index');

$eventos->get('/{slug}/', function ($slug) use($app) {
    ...
})->bind('eventos_show');

$foro = $app['controllers_factory'];

$foro->get('/{id}/{slug}/', function($id, $slug) use($app) {
    ...
})->bind('foro_show');

$foro->get('/', function () use ($app) {
    ...
})->bind('foro_index');

$app->mount('/eventos', $eventos);
$app->mount('/foro', $foro);

$app->get('/', function() use($app) {
    ...
})->bind('portada');

$app->run();

Cada sección de la aplicación (foro y eventos) se agrupa en una colección de controladores mediante el servicio $app['controllers_factory'] y después se incluyen en la aplicación con el método mount().

La primera ventaja es que puedes aplicar un mismo prefijo a todas las rutas relacionadas. Por eso en el código anterior, la ruta foro_index no es /foro sino simplemente /, ya que el prefijo /foro se incluye automáticamente al montar la colección de controladores.

La segunda ventaja es que ahora resulta muy sencillo reordenar las rutas de la aplicación, ya que para ello simplemente debes cambiar el orden de las instrucciones mount(). Por último, las colecciones también permiten aplicar fácilmente un middleware a un conjunto de rutas, como muestra el siguiente ejemplo que restringe el acceso a la sección del foro:

// ...

$app = new Silex\Application();

$foro = $app['controllers_factory'];
$foro->get('/{id}/{slug}/', ...)->bind('foro_show');
$foro->get('/', ...)->bind('foro_index');

// restringir el acceso a todas las rutas del foro
$foro->before(function() use($app) {
    if (!$app['security']->isGranted('ROLE_ADMIN')) {
        // ...
    }
});

$app->mount('/foro', $foro);

// ...

La principal desventaja de este método es que no mejora mucho la situación inicial. Como todo el código sigue estando en un único archivo, resulta difícil manejarlo. Una posible solución consiste en colocar el código de cada sección en su propio archivo e importarlo después dentro de la instrucción mount():

// archivo foro.php
$foro = $app['controllers_factory'];
$foro->get('/{id}/{slug}/', ...)->bind('foro_show');
$foro->get('/', ...)->bind('foro_index');

return $foro;

// archivo index.php
$app = new Silex\Application();

// ...
$app->mount('/foro', include __DIR__.'/foro.php');

$app->run();

Ahora el código sí que está dividido en varios archivos más pequeños y manejables. El único problema de esta solución es que el uso del include la hace poco elegante. Los dos métodos que se muestran a continuación consiguen un resultado similar pero de forma mucho más profesional.

Método 2: Controladores en clases PHP normales

Este segundo método es similar al explicado en la sección anterior, pero se basa en crear una clase PHP para cada sección del sitio. Dentro de esa clase, se define un método para cada ruta/controlador de esa sección. El siguiente ejemplo muestra la clase para la sección del foro:

// archivo src/Controllers/ForoController.php
namespace Librosweb\Controllers;

use Silex\Application;

class ForoController
{
    public function show(Application $app, $id, $slug)
    {
        ...
    }

    public function index(Application $app)
    {
        ...
    }
}

Aunque puedes llamar como quieras a estas clases y puedes guardarlas en cualquier sitio, nuestra recomendación para organizar mejor el código es que guardes estas clases en el directorio Controllers y que normalices sus nombres para que siempre acaben en Controller.

Si necesitas acceder a la aplicación o a la petición del usuario dentro de un controlador, sólo tienes que añadir respectivamente Application $app o Request $request en la declaración del método y Silex los inyectará automáticamente.

Una vez creadas todas estas clases, debes modificar el anterior archivo index.php para definir en él solamente las rutas de la aplicación, no los controladores:

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

$app = new Silex\Application();

$app->get('/eventos/{slug}/', 'Librosweb\Controllers\EventosController::show')
    ->bind('eventos_show');
$app->get('/eventos/', 'Librosweb\Controllers\EventosController::index')
    ->bind('eventos_index');

$app->get('/foro/{id}/{slug}/', 'Librosweb\Controllers\ForoController::show')
    ->bind('foro_show');
$app->get('/foro/', 'Librosweb\Controllers\ForoController::index')
    ->bind('foro_index');

$app->get('/', function() use($app) {
    ...
})->bind('portada');

$app->run();

Cada ruta define su controlador asociado mediante la notación Clase::metodo. No confundas la notación :: con el operador :: de PHP que se utiliza para acceder a los métodos estáticos de las clases, ya que no tienen nada que ver.

La principal ventaja de este método es que el código se divide en varias clases pequeñas y manejables. Además, el rendimiento de la aplicación no se resiente porque cuando el usuario realiza una petición, solamente se instancia la clase asociada a esa ruta.

La principal desventaja es que si tienes muchas rutas, el archivo index.php de nuevo es demasiado grande y se vuelve difícil de manejar.

Método 3: Controladores en clases de tipo ControllerProvider

Esta última forma de organizar el código de las aplicaciones Silex combina la mayor parte de las ventajas de los otros dos métodos y tiene muy pocos inconvenientes. Por eso creemos que es la mejor forma de organizar el código de las aplicaciones Silex medianas y es el método que utilizamos actualmente para el código fuente de librosweb.es.

El funcionamiento de este método se basa en el uso de los proveedores de controladores de Silex. Cada sección del sitio web se convierte en una clase PHP especial y después se montan todas ellas con el método mount() de la aplicación. Observa en primer lugar el código completo del archivo index.php y cómo se importan las dos clases:

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

$app = new Silex\Application();

$app->mount('/eventos', new Librosweb\Controllers\EventosController());
$app->mount('/foro',    new Librosweb\Controllers\ForoController());

$app->get('/', function() use($app) {
    ...
})->bind('portada');

$app->run();

Las clases EventosController y ForoController son similares a las clases mostradas en la sección anterior. La diferencia es que estas clases deben implementar la interfaz ControllerProviderInterface:

// archivo src/Controllers/ForoController.php
namespace Librosweb\Controllers;

use Silex\Application;
use Silex\ControllerProviderInterface;

class ForoController implements ControllerProviderInterface
{
    // ...
}

La interfaz ControllerProviderInterface obliga a definir un método llamado connect() que devuelve la colección de controladores creada por esta clase. Así que el código completo de la clase ForoController de este ejemplo podría ser el siguiente:

// archivo src/Controllers/EventosController.php
namespace Librosweb\Controllers;

use Silex\Application;
use Silex\ControllerProviderInterface;

class ForoController implements ControllerProviderInterface
{
    public function connect(Application $app)
    {
        $controllers = $app['controllers_factory'];

        $controllers->get('/{id}/{slug}/', function($id, $slug) use($app) {
            ...
        })->bind('foro_show');

        $controllers->get('/', function() use($app) {
            ...
        })->bind('foro_index');

        return $controllers;
    }
}

Una última variante de este método consiste en indicar un callback para cada controlador, en vez de escribir todo su código mediante una función anónima. Así que el siguiente ejemplo muestra el código completo tal y como realmente lo utilizo en mis aplicaciones:

// archivo src/Controllers/EventosController.php
namespace Librosweb\Controllers;

use Silex\Application;
use Silex\ControllerProviderInterface;

class ForoController implements ControllerProviderInterface
{
    public function connect(Application $app)
    {
        $controllers = $app['controllers_factory'];

        $controllers
            ->get('/{id}/{slug}/', array($this, 'foroShow'))
            ->bind('foro_show')
        ;

        $controllers
            ->get('/', array($this, 'foroIndex'))
            ->bind('foro_index')
        ;

        return $controllers;
    }

    public function foroIndex(Application $app)
    {
        ...
    }

    public function foroShow(Application $app, $id, $slug)
    {
        ...
    }
}

Por último, este método también permite aplicar fácilmente un middleware a toda la colección de controladores. Así es muy sencillo por ejemplo restringir el acceso a una parte de la aplicación:

// archivo src/Controllers/EventosController.php
namespace Librosweb\Controllers;

use Silex\Application;
use Silex\ControllerProviderInterface;

class ForoController implements ControllerProviderInterface
{
    public function connect(Application $app)
    {
        $controllers = $app['controllers_factory'];

        // restringir el acceso a todas las rutas del foro
        $controllers->before(function() use($app) {
            if (!$app['security']->isGranted('ROLE_ADMIN')) {
                // ...
            }
        });

        $controllers->get('/{id}/{slug}/', ...)->bind('foro_show');
        $controllers->get('/', ...)->bind('foro_index');

        return $controllers;
    }
}

Las ventajas de este método son que el archivo index.php es pequeño y manejable, que resulta muy sencillo montar los controladores con un prefijo común (/foro, /eventos, etc.) y que las rutas de la aplicación se pueden reordenar fácilmente. Además, el código de cada sección del sitio se define en su propia clase, también pequeña y manejable, que incluye tanto las rutas como los controladores de esa sección.

La única desventaja notable que he encontrado a este método es que cuando el usuario realiza una petición, Silex instancia todas y cada una de las clases de tipo ControllerProviderInterface de la aplicación y no solamente la que está asociada con la ruta solicitada por el usuario. Esto puede penalizar ligeramente el rendimiento de la aplicación cuando se divide el código en muchas clases diferentes.

Referencias útiles

La mejor forma de aprender a organizar las aplicaciones medianas con Silex consiste en ver el código fuente de alguna aplicación real. Como el código de librosweb.es no es público, enlazamos a continuación el código de dos aplicaciones Silex medianas que están programadas de una manera muy similar:

  • Bolt, gestor de contenidos que organiza su código siguiendo el método 2 (ver ejemplo).
  • GitList, clon del sitio GitHub y que organiza su código siguiendo el método 3 (ver ejemplo).