Symfony2 frente a PHP simple

¿Por qué Symfony2 es mejor que sólo abrir un archivo y escribir PHP simple?

Si nunca has usado una plataforma PHP, no estás familiarizado con la filosofía MVC, o simplemente te preguntas qué es todo ese alboroto en torno a Symfony2, este capítulo es para ti. En vez de decirte que Symfony2 te permite desarrollar software más rápido y mejor que con PHP simple, debes verlo tú mismo.

En este capítulo, vamos a escribir una aplicación sencilla en PHP simple, y luego la reconstruiremos para que esté mejor organizada. Podrás viajar a través del tiempo, viendo las decisiones de por qué el desarrollo web ha evolucionado en los últimos años hasta donde está ahora.

Al final, verás cómo Symfony2 te puede rescatar de las tareas cotidianas y te permite recuperar el control de tu código.

Un sencillo blog en PHP simple

En este capítulo, crearemos una simbólica aplicación de blog utilizando sólo PHP simple. Para empezar, crea una página que muestre las entradas del blog que se han persistido en la base de datos. Escribirla en PHP simple es rápido y sucio:

<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);

$result = mysql_query('SELECT id, title FROM post', $link);
?>

<!DOCTYPE html>
    <html>
        <head>
        <title>List of Posts</title>
        </head>
        <body>
        <h1>List of Posts</h1>
        <ul>
            <?php while ($row = mysql_fetch_assoc($result)): ?>
            <li>
                <a href="/show.php?id=<?php echo $row['id'] ?>">
                    <?php echo $row['title'] ?>
                    </a>
            </li>
            <?php endwhile; ?>
        </ul>
    </body>
</html>

<?php
mysql_close($link);
?>

Eso es fácil de escribir, se ejecuta rápido, y, cuando tu aplicación crece, imposible de mantener. Hay varios problemas que es necesario abordar:

  • No hay comprobación de errores: ¿Qué sucede si falla la conexión a la base de datos?
  • Deficiente organización: Si la aplicación crece, este único archivo cada vez será más difícil de mantener, hasta que finalmente sea imposible. ¿Dónde se debe colocar el código para manejar un formulario enviado? ¿Cómo se pueden validar los datos? ¿Dónde debe ir el código para enviar mensajes de correo electrónico?
  • Es difícil reutilizar el código: Ya que todo está en un archivo, no hay manera de volver a utilizar alguna parte de la aplicación en otras «páginas» del blog.

Nota

Otro problema no mencionado aquí es el hecho de que la base de datos está vinculada a MySQL. Aunque no se ha tratado aquí, Symfony2 integra Doctrine plenamente, una biblioteca dedicada a la abstracción y asignación de bases de datos.

Trabajaremos en la solución de estos y muchos problemas más.

Aislando la presentación

El código inmediatamente se puede beneficiar de la separación entre la «lógica» de la aplicación y el código que prepara la «presentación» HTML:

<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);

$result = mysql_query('SELECT id, title FROM post', $link);

$posts = array();
while ($row = mysql_fetch_assoc($result)) {
    $posts[] = $row;
}

mysql_close($link);

// incluye el código HTML de la presentación
require 'templates/list.php';

Ahora el código HTML está guardado en un archivo separado (templates/list.php), el cual principalmente es un archivo HTML que utiliza una sintaxis de plantilla tipo PHP:

<!DOCTYPE html>
    <html>
        <head>
        <title>List of Posts</title>
        </head>
        <body>
        <h1>List of Posts</h1>
        <ul>
            <?php foreach ($posts as $post): ?>
            <li>
                <a href="/read?id=<?php echo $post['id'] ?>">
                    <?php echo $post['title'] ?>
                    </a>
            </li>
            <?php endforeach; ?>
        </ul>
    </body>
</html>

Por convención, el archivo que contiene toda la lógica de la aplicación —index.php— se conoce como «controlador». El término controlador es una palabra que se escucha mucho, independientemente del lenguaje o plataforma que utilices. Simplemente se refiere a la zona de tu código que procesa la entrada del usuario y prepara la respuesta.

En este caso, el controlador prepara los datos de la base de datos y, luego los incluye en una plantilla para presentarlos. Con el controlador aislado, fácilmente podríamos cambiar sólo el archivo de plantilla si es necesario procesar las entradas del blog en algún otro formato (por ejemplo, lista.json.php para el formato JSON).

Aislando la lógica de la aplicación (el dominio)

Hasta ahora, la aplicación sólo contiene una página. Pero ¿qué pasa si una segunda página necesita utilizar la misma conexión a la base de datos, e incluso el mismo arreglo de entradas del blog? Reconstruye el código para que el comportamiento de las funciones básicas de acceso a datos de la aplicación esté aislado en un nuevo archivo llamado model.php:

<?php
// model.php
function open_database_connection()
{
    $link = mysql_connect('localhost', 'myuser', 'mypassword');
    mysql_select_db('blog_db', $link);

    return $link;
}

function close_database_connection($link)
{
    mysql_close($link);
}

function get_all_posts()
{
    $link = open_database_connection();

    $result = mysql_query('SELECT id, title FROM post', $link);
    $posts = array();
    while ($row = mysql_fetch_assoc($result)) {
        $posts[] = $row;
    }
    close_database_connection($link);

    return $posts;
}

Truco

Utilizamos el nombre de archivo model.php debido a que el acceso a la lógica y los datos de una aplicación, tradicionalmente, se conoce como la capa del «modelo». En una aplicación bien organizada, la mayoría del código que representa tu «lógica de negocio» debe vivir en el modelo (en lugar de vivir en un controlador). Y, a diferencia de este ejemplo, sólo una parte (o ninguna) del modelo realmente está interesada en acceder a la base de datos.

El controlador (index.php) ahora es muy sencillo:

<?php
require_once 'model.php';

$posts = get_all_posts();

require 'templates/list.php';

Ahora, la única tarea del controlador es conseguir los datos de la capa del modelo de la aplicación (el modelo) e invocar a una plantilla que reproduce los datos. Este es un ejemplo muy simple del patrón modelo-vista-controlador.

Aislando el diseño

En este punto, reconstruiste la aplicación en tres piezas distintas, mismas que ofrecen varias ventajas y la oportunidad de volver a utilizar casi todo en diferentes páginas.

La única parte del código que no se puede reutilizar es el diseño de la página. Corregirás esto creando un nuevo archivo base.php:

<!-- templates/base.php -->
<!DOCTYPE html>
    <html>
        <head>
        <title><?php echo $title ?></title>
        </head>
        <body>
        <?php echo $content ?>
    </body>
</html>

La plantilla (templates/list.php) ahora se puede simplificar para «extender» el diseño:

<?php $title = 'List of Posts' ?>

<?php ob_start() ?>
    <h1>List of Posts</h1>
    <ul>
        <?php foreach ($posts as $post): ?>
        <li>
            <a href="/read?id=<?php echo $post['id'] ?>">
                <?php echo $post['title'] ?>
                </a>
        </li>
        <?php endforeach; ?>
    </ul>
<?php $content = ob_get_clean() ?>

<?php include 'base.php' ?>

Ahora se introdujo una metodología que te permite reutilizar el diseño. Desafortunadamente, para lograrlo, estás obligado a utilizar en la plantilla algunas desagradables funciones de PHP (ob_start(), ob_get_clean()). Symfony2 utiliza un componente Templating que te permite realizar esto limpia y fácilmente. En breve lo verás en acción.

Agregando una página «show» al blog

La página «list» del blog se ha rediseñado para que el código esté mejor organizado y sea reutilizable. Para probarlo, añade una página «show» al blog, que muestre una entrada individual del blog identificada por un parámetro de consulta id.

Para empezar, crea una nueva función en el archivo model.php que recupere un resultado individual del blog basándose en un identificador dado:

// model.php
function get_post_by_id($id)
{
    $link = open_database_connection();

    $id = intval($id);
    $query = 'SELECT date, title, body FROM post WHERE id = '.$id;
    $result = mysql_query($query);
    $row = mysql_fetch_assoc($result);

    close_database_connection($link);

    return $row;
}

A continuación, crea un nuevo archivo llamado show.php —el controlador para esta nueva página:

<?php
require_once 'model.php';

$post = get_post_by_id($_GET['id']);

require 'templates/show.php';

Por último, crea el nuevo archivo de plantilla —templates/show.php— para reproducir una entrada individual del blog:

<?php $title = $post['title'] ?>

<?php ob_start() ?>
    <h1><?php echo $post['title'] ?></h1>

    <div class="date"><?php echo $post['date'] ?></div>
    <div class="body">
        <?php echo $post['body'] ?>
    </div>
<?php $content = ob_get_clean() ?>

<?php include 'base.php' ?>

Ahora, es muy fácil crear la segunda página y sin duplicar código. Sin embargo, esta página introduce problemas aún más perniciosos que una plataforma puede resolver por ti. Por ejemplo, un parámetro id ilegal u omitido en la consulta hará que la página se bloquee. Sería mejor si esto reprodujera una página 404, pero sin embargo, en realidad esto no se puede hacer fácilmente. Peor aún, si olvidaras desinfectar el parámetro id por medio de la función intval(), tu base de datos estaría en riesgo de un ataque de inyección SQL.

Otro importante problema es que cada archivo de controlador individual debe incluir al archivo model.php. ¿Qué pasaría si cada archivo de controlador de repente tuviera que incluir un archivo adicional o realizar alguna tarea global (por ejemplo, reforzar la seguridad)? Tal como está ahora, el código tendría que incluir todos los archivos de los controladores. Si olvidas incluir algo en un solo archivo, esperemos que no sea alguno relacionado con la seguridad...

El «controlador frontal» al rescate

Una mucho mejor solución es usar un controlador frontal: un único archivo PHP a través del cual se procesen todas las peticiones. Con un controlador frontal, la URI de la aplicación cambia un poco, pero se vuelve más flexible:

Sin controlador frontal
/index.php          => (ejecuta index.php) la página lista de mensajes.
/show.php           => (ejecuta show.php)  la página muestra un mensaje particular.

Con index.php como controlador frontal
/index.php          => (ejecuta index.php) la página lista de mensajes.
/index.php/show     => (ejecuta index.php) la página muestra un mensaje particular.

Truco

Puedes quitar la porción index.php de la URI si utilizas las reglas de reescritura de Apache (o equivalentes). En ese caso, la URI resultante de la página show del blog simplemente sería /show.

Cuando se usa un controlador frontal, un solo archivo PHP (index.php en este caso) procesa todas las peticiones. Para la página show del blog, /index.php/show realmente ejecuta el archivo index.php, que ahora es el responsable de dirigir internamente las peticiones basándose en la URI completa. Como puedes ver, un controlador frontal es una herramienta muy poderosa.

Creando el controlador frontal

Estás a punto de dar un gran paso en la aplicación. Con un archivo manejando todas las peticiones, puedes centralizar cosas tales como el manejo de la seguridad, la carga de configuración y enrutado. En esta aplicación, index.php ahora debe ser lo suficientemente inteligente como para reproducir la lista de entradas del blog o mostrar la página de una entrada particular basándose en la URI solicitada:

<?php
// index.php

// carga e inicia algunas bibliotecas globales
require_once 'model.php';
require_once 'controllers.php';

// encamina la petición internamente
$uri = $_SERVER['REQUEST_URI'];
if ('/index.php' == $uri) {
    list_action();
} elseif ('/index.php/show' == $uri && isset($_GET['id'])) {
    show_action($_GET['id']);
} else {
    header('Status: 404 Not Found');
    echo '<html><body><h1>Page Not Found</h1></body></html>';
}

Por organización, ambos controladores (antes index.php y show.php) son funciones PHP y cada una se ha movido a un archivo separado, controllers.php:

function list_action()
{
    $posts = get_all_posts();
    require 'templates/list.php';
}

function show_action($id)
{
    $post = get_post_by_id($id);
    require 'templates/show.php';
}

Como controlador frontal, index.php ha asumido un papel completamente nuevo, el cual incluye la carga de las bibliotecas del núcleo y encaminar la aplicación para invocar a uno de los dos controladores (las funciones list_action() y show_action()). En realidad, el controlador frontal está empezando a verse y actuar como el mecanismo Symfony2 para la manipulación y enrutado de peticiones.

Truco

Otra ventaja del controlador frontal es la flexibilidad de las URL. Ten en cuenta que la URL a la página show del blog se puede cambiar de /show a /read cambiando el código solamente en una única ubicación. Antes, era necesario cambiar todo un archivo para cambiar el nombre. En Symfony2, incluso las URL son más flexibles.

Por ahora, la aplicación ha evolucionado de un único archivo PHP, a una estructura organizada y permite la reutilización de código. Debes estar feliz, pero aún lejos de estar satisfecho. Por ejemplo, el sistema de «enrutado» es voluble, y no reconoce que la página list (/index.php) también debe ser accesible a través de / (si has agregado las reglas de reescritura de Apache). Además, en lugar de desarrollar el blog, una gran cantidad del tiempo se ha gastado trabajando en la «arquitectura» del código (por ejemplo, el enrutado, invocando controladores, plantillas, etc.) Se tendrá que gastar más tiempo para manejar el envío de formularios, validación de entradas, llevar la bitácora de sucesos y la seguridad. ¿Por qué tienes que reinventar soluciones a todos estos problemas rutinarios?

Añadiendo un toque Symfony2

Symfony2 al rescate. Antes de utilizar Symfony2 realmente, necesitas descargarlo. Esto se puede hacer utilizando Composer, el cual cuida de descargar la versión correcta con todas sus dependencias y proporciona un cargador automático. Un cargador automático es una herramienta que permite empezar a utilizar clases PHP sin incluir explícitamente el archivo que contiene la clase.

En tu directorio raíz crea un archivo de texto llamado composer.json con el siguiente contenido:

{
    "require": {
        "symfony/symfony": "2.2.*"
    },
    "autoload": {
        "files": ["model.php","controllers.php"]
    }
}

Luego, descarga Composer y entonces ejecuta la siguiente orden, la cual descargará Symfony a un directorio vendor/:

$ php composer.phar install

Junto con la descarga de tus dependencias, Composer genera un archivo vendor/autoload.php, el cual cuida de cargar automáticamente todos los archivos en la platafomra Symfony así como los archivos mencionados en la sección autoload de tu composer.json.

La esencia de la filosofía Symfony es la idea de que el trabajo principal de una aplicación es interpretar cada petición y devolver una respuesta. Con este fin, Symfony2 proporciona ambas clases Symfony\Component\HttpFoundation\Request y Symfony\Component\HttpFoundation\Response. Estas clases son representaciones orientadas a objetos de la petición HTTP que se está procesando y la respuesta HTTP que devolverá. Úsalas para mejorar el blog:

<?php
// index.php
require_once 'vendor/autoload.php';

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

$request = Request::createFromGlobals();

$uri = $request->getPathInfo();
if ('/' == $uri) {
    $response = list_action();
} elseif ('/show' == $uri && $request->query->has('id')) {
    $response = show_action($request->query->get('id'));
} else {
    $html = '<html><body><h1>Page Not Found</h1></body></html>';
    $response = new Response($html, 404);
}

// difunde las cabeceras y envía la respuesta
$response->send();

Los controladores son responsables de devolver un objeto Respuesta. Para facilitarnos esto, puedes agregar una nueva función render_template(), la cual, por cierto, actúa un poco como el motor de plantillas de Symfony2:

// controllers.php
use Symfony\Component\HttpFoundation\Response;

function list_action()
{
    $posts = get_all_posts();
    $html = render_template('templates/list.php', array('posts' => $posts));

    return new Response($html);
}

function show_action($id)
{
    $post = get_post_by_id($id);
    $html = render_template('templates/show.php', array('post' => $post));

    return new Response($html);
}

// función ayudante para reproducir plantillas
function render_template($path, array $args)
{
    extract($args);
    ob_start();
    require $path;
    $html = ob_get_clean();

    return $html;
}

Al reunir una pequeña parte de Symfony2, la aplicación es más flexible y fiable. La Petición proporciona una manera confiable para acceder a información de la petición HTTP. Especialmente, el método getPathInfo() devuelve una URI limpia (siempre devolviendo /show y nunca /index.php/show). Por lo tanto, incluso si el usuario va a /index.php/show, la aplicación es lo suficientemente inteligente para encaminar la petición hacia show_action().

El objeto Respuesta proporciona flexibilidad al construir la respuesta HTTP, permitiendo que las cabeceras HTTP y el contenido se agreguen a través de una interfaz orientada a objetos. Y aunque las respuestas en esta aplicación son simples, esta flexibilidad pagará dividendos en cuanto tu aplicación crezca.

Aplicación de ejemplo en Symfony2

El blog ha avanzado, pero todavía contiene una gran cantidad de código para una aplicación tan simple. De paso, también inventamos un sencillo sistema de enrutado y un método utilizando ob_start() y ob_get_clean() para procesar plantillas. Si por alguna razón, necesitas continuar la construcción de esta «plataforma» desde cero, por lo menos puedes usar los componentes independientes Routing y Templating de Symfony, que resuelven estos problemas.

En lugar de volver a solucionar problemas comunes, puedes dejar que Symfony2 se preocupe de ellos por ti. Aquí está la misma aplicación de ejemplo, ahora construida en Symfony2:

// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function listAction()
    {
        $posts = $this->get('doctrine')->getManager()
            ->createQuery('SELECT p FROM AcmeBlogBundle:Post p')
            ->execute();

        return $this->render(
            'AcmeBlogBundle:Blog:list.html.php',
            array('posts' => $posts)
        );
    }

    public function showAction($id)
    {
        $post = $this->get('doctrine')
            ->getManager()
            ->getRepository('AcmeBlogBundle:Post')
            ->find($id)
        ;

        if (!$post) {
            // provoca que se muestre la página de error 404
            throw $this->createNotFoundException();
        }

        return $this->render(
            'AcmeBlogBundle:Blog:show.html.php',
            array('post' => $post)
        );
    }
}

Los dos controladores siguen siendo ligeros. Cada uno utiliza la biblioteca ORM de Doctrine para recuperar objetos de la base de datos y el componente Templating para reproducir una plantilla y devolver un objeto Respuesta. La plantilla list ahora es un poco más simple:

<!-- src/Acme/BlogBundle/Resources/views/Blog/list.html.php -->
<?php $view->extend('::layout.html.php') ?>

<?php $view['slots']->set('title', 'List of Posts') ?>

<h1>List of Posts</h1>
<ul>
    <?php foreach ($posts as $post): ?>
    <li>
        <a href="<?php echo $view['router']->generate(
            'blog_show',
            array('id' => $post->getId())
        ) ?>">
            <?php echo $post->getTitle() ?>
            </a>
    </li>
    <?php endforeach; ?>
</ul>

El diseño es casi idéntico:

<!-- app/Resources/views/layout.html.php -->
<!DOCTYPE html>
    <html>
        <head>
        <title><?php echo $view['slots']->output(
            'title',
            'Default title'
        ) ?></title>
        </head>
        <body>
        <?php echo $view['slots']->output('_content') ?>
    </body>
</html>

Nota

Te vamos a dejar como ejercicio la plantilla show, porque debería ser trivial crearla basándote en la plantilla list.

Cuando arranca el motor Symfony2 (conocido como el núcleo), necesita un mapa para saber qué controladores ejecutar basándose en la información solicitada. Un mapa de configuración de enrutado proporciona esta información en formato legible:

# app/config/routing.yml
blog_list:
    path:     /blog
    defaults: { _controller: AcmeBlogBundle:Blog:list }

blog_show:
    path:     /blog/show/{id}
    defaults: { _controller: AcmeBlogBundle:Blog:show }

Ahora que Symfony2 se encarga de todas las tareas rutinarias, el controlador frontal es muy simple. Y debido a que hace tan poco, nunca tienes que volver a tocarlo una vez creado (y si utilizas una distribución Symfony2, ¡ni siquiera tendrás que crearlo!):

// web/app.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();

El único trabajo del controlador frontal es iniciar el motor de Symfony2 (núcleo) y pasarle un objeto Petición para que lo manipule. El núcleo de Symfony2 entonces utiliza el mapa de enrutado para determinar qué controlador invocar. Al igual que antes, el método controlador es el responsable de devolver el objeto Respuesta final. Realmente no hay mucho más sobre él.

Para conseguir una representación visual de cómo maneja Symfony2 cada petición, consulta el diagrama de flujo de la petición.

Qué más ofrece Symfony2

En los siguientes capítulos, aprenderás más acerca de cómo funciona cada pieza de Symfony y la organización recomendada de un proyecto. Por ahora, vamos a ver cómo, migrar el blog de PHP simple a Symfony2 nos ha mejorado la vida:

  • Tu aplicación cuenta con código claro y organizado consistentemente (aunque Symfony no te obliga a ello). Este promueve la reutilización y permite a los nuevos desarrolladores ser productivos en el proyecto con mayor rapidez.
  • 100% del código que escribes es para tu aplicación. No necesitas desarrollar o mantener servicios públicos de bajo nivel tal como la carga automática de clases, el enrutado o la reproducción de controladores;
  • Symfony2 te proporciona acceso a herramientas de código abierto tal como Doctrine, plantillas, seguridad, formularios, validación y componentes de traducción (por nombrar algunos);
  • La aplicación ahora disfruta de direcciones URL totalmente flexibles gracias al componente Routing;
  • La arquitectura centrada en HTTP de Symfony2 te da acceso a poderosas herramientas, tal como la memoria caché HTTP impulsadas por la caché HTTP interna de Symfony2 o herramientas más poderosas, tales como Varnish. Esto se trata posteriormente en el capítulo «todo sobre caché».

Y lo mejor de todo, utilizando Symfony2, ¡ahora tienes acceso a un conjunto de herramientas de código abierto de alta calidad desarrolladas por la comunidad Symfony2! Puedes encontrar una buena colección de herramientas comunitarias de Symfony2 en KnpBundles.com.

Mejores plantillas

Si decides utilizarlo, Symfony2 de serie viene con un motor de plantillas llamado Twig el cual hace que las plantillas se escriban más rápido y sean más fáciles de leer. Esto significa que, incluso, ¡la aplicación de ejemplo podría contener mucho menos código! Toma por ejemplo, la plantilla list escrita en Twig:

{# src/Acme/BlogBundle/Resources/views/Blog/list.html.twig #}
{% extends "::layout.html.twig" %}

{% block title %}List of Posts{% endblock %}

{% block body %}
    <h1>List of Posts</h1>
    <ul>
        {% for post in posts %}
        <li>
            <a href="{{ path('blog_show', {'id': post.id}) }}">
                {{ post.title }}
                </a>
        </li>
        {% endfor %}
    </ul>
{% endblock %}

También es fácil escribir la plantilla layout.html.twig correspondiente:

{# app/Resources/views/layout.html.twig #}
<!DOCTYPE html>
    <html>
        <head>
        <title>{% block title %}Default title{% endblock %}</title>
        </head>
        <body>
        {% block body %}{% endblock %}
    </body>
</html>

Twig es compatible con Symfony2. Y si bien, las plantillas PHP siempre contarán con el apoyo de Symfony2, vamos a seguir explicando muchas de las ventajas de Twig. Para más información, consulta el capítulo Plantillas.

Bifúrcame en GitHub