Symfony contro PHP puro

Perché Symfony è meglio che aprire un file e scrivere PHP puro?

Questo capitolo è per chi non ha mai usato un framework PHP, non ha familiarità con la filosofia MVC, oppure semplicemente si chiede il motivo di tutto il clamore su Symfony. Invece di raccontare che Symfony consente di sviluppare software più rapidamente e in modo migliore che con PHP puro, ve lo faremo vedere.

In questo capitolo, scriveremo una semplice applicazione in PHP puro e poi la rifattorizzeremo per essere più organizzata. Viaggeremo nel tempo, guardando le decisioni che stanno dietro ai motivi per cui lo sviluppo web si è evoluto durante gli ultimi anni per diventare quello che è ora.

Alla fine, vedremo come Symfony possa salvarci da compiti banali e consentirci di riprendere il controllo del nostro codice.

Un semplice blog in PHP puro

In questo capitolo, costruiremo un’applicazione blog usando solo PHP puro. Per iniziare, creiamo una singola pagina che mostra le voci del blog, che sono state memorizzate nella base dati. La scrittura in puro PHP è sporca e veloce:

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

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

<!DOCTYPE html>
<html>
    <head>
        <title>Lista dei post</title>
    </head>
    <body>
        <h1>Lista dei post</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);
?>

Veloce da scrivere, rapido da eseguire e, al crescere dell’applicazione, impossibile da mantenere. Ci sono diversi problemi che occorre considerare:

  • Niente verifica degli errori: Che succede se la connessione alla base dati fallisce?
  • Scarsa organizzazione: Se l’applicazione cresce, questo singolo file diventerà sempre più immantenibile. Dove inserire il codice per gestire la compilazione di un form? Come validare i dati? Dove mettere il codice per inviare delle email?
  • Difficoltà nel riusare il codice: Essendo tutto in un solo file, non c’è modo di riusare alcuna parte dell’applicazione per altre “pagine” del blog.

Note

Un altro problema non menzionato è il fatto che la base dati è legata a MySQL. Sebbene non affrontato qui, Symfony integra in pieno Doctrine, una libreria dedicata all’astrazione e alla mappatura della base dati.

Isolare la presentazione

Il codice può beneficiare immediatamente dalla separazione della “logica” dell’applicazione dal codice che prepara la “presentazione” in HTML:

<?php
// index.php
$link = mysql_connect('localhost', 'mioutente', 'miapassword');
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);

// include il codice HTML di presentazione
require 'templates/list.php';

Il codice HTML ora è in un file separato (templates/list.php), che è essenzialmente un file HTML che usa una sintassi PHP per template:

<!DOCTYPE html>
<html>
    <head>
        <title>Lista dei post</title>
    </head>
    <body>
        <h1>Lista dei post</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>

Per convenzione, il file che contiene tutta la logica dell’applicazione, cioè index.php, è noto come “controllore”. Il termine controllore è una parola che ricorrerà spesso, quale che sia il linguaggio o il framework scelto. Si riferisce semplicemente alla parte del proprio codice che processa l’input proveniente dall’utente e prepara la risposta.

In questo caso, il nostro controllore prepara i dati estratti dalla base dati e quindi include un template, per presentare tali dati. Con il controllore isolato, è possibile cambiare facilmente solo il file template necessario per rendere le voci del blog in un qualche altro formato (p.e. list.json.php per il formato JSON).

Isolare la logica dell’applicazione (il dominio)

Finora l’applicazione contiene una singola pagina. Ma se una seconda pagina avesse bisogno di usare la stessa connessione alla base dati, o anche lo stesso array di post del blog? Rifattorizziamo il codice in modo che il comportamento centrale e le funzioni di accesso ai dati dell’applicazioni siano isolati in un nuovo file, chiamato model.php:

<?php
// model.php
function open_database_connection()
{
    $link = mysql_connect('localhost', 'mioutente', 'miapassword');
    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;
}

Tip

Il nome model.php è usato perché la logica e l’accesso ai dati di un’applicazione sono tradizionalmente noti come il livello del “modello”. In un’applicazione ben organizzata la maggior parte del codice che rappresenta la “logica di business” dovrebbe stare nel modello (invece che stare in un controllore). Diversamente da questo esempio, solo una parte (o niente) del modello riguarda effettivamente l’accesso a una base dati.

Il controllore (index.php) è ora molto semplice:

<?php
require_once 'model.php';

$posts = get_all_posts();

require 'templates/list.php';

Ora, l’unico compito del controllore è prendere i dati dal livello del modello dell’applicazione (il modello) e richiamare un template per rendere tali dati. Questo è un esempio molto semplice del pattern model-view-controller.

Isolare il layout

A questo punto, l’applicazione è stata rifattorizzata in tre parti distinte, offrendo diversi vantaggi e l’opportunità di riusare quasi tutto su pagine diverse.

L’unica parte del codice che non può essere riusata è il layout. Sistemiamo questo aspetto, creando un nuovo file layout.php:

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

Il template (templates/list.php) ora può essere semplificato, per “estendere” il layout:

<?php $title = 'Lista dei post' ?>

<?php ob_start() ?>
    <h1>Lista dei post</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 'layout.php' ?>

Qui abbiamo introdotto una metodologia che consente il riuso del layout. Sfortunatamente, per poterlo fare, si è costretti a usare alcune brutte funzioni PHP (ob_start(), ob_get_clean()) nel template. Symfony usa un componente Templating, che consente di poter fare ciò in modo pulito e facile. Lo vedremo in azione tra poco.

Aggiungere al blog una pagina “show”

La pagina “elenco” del blog è stata ora rifattorizzata in modo che il codice sia meglio organizzato e riusabile. Per provarlo, aggiungiamo al blog una pagina “mostra”, che mostra un singolo post del blog identificato dal parametro id.

Per iniziare, creiamo nel file model.php una nuova funzione, che recupera un singolo risultato del blog a partire da un id dato:

// 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;
}

Quindi, creiamo un file chiamato show.php, il controllore per questa nuova pagina:

<?php
require_once 'model.php';

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

require 'templates/show.php';

Infine, creiamo un nuovo file template, templates/show.php, per rendere il singolo post 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 'layout.php' ?>

La creazione della seconda pagina è stata molto facile e non ha implicato alcuna duplicazione di codice. Tuttavia, questa pagina introduce alcuni altri problemi, che un framework può risolvere. Per esempio, un parametro id mancante o non valido causerà un errore nella pagina. Sarebbe meglio se facesse rendere una pagina 404, ma non possiamo ancora farlo in modo facile. Inoltre, avendo dimenticato di pulire il parametro id con la funzione mysql_real_escape_string(), la base dati è a rischio di attacchi di tipo SQL injection.

Un altro grosso problema è che ogni singolo controllore deve includere il file model.php. Che fare se poi occorresse includere un secondo file o eseguire un altro compito globale (p.e. garantire la sicurezza)? Nella situazione attuale, tale codice dovrebbe essere aggiunto a ogni singolo file. Se lo si dimentica in un file, speriamo che non sia qualcosa legato alla sicurezza.

Un “front controller” alla riscossa

La soluzione è usare un front controller: un singolo file PHP attraverso il quale tutte le richieste sono processate. Con un front controller, gli URI dell’applicazione cambiano un poco, ma iniziano a diventare più flessibili:

Senza un front controller
/index.php          => Pagina della lista dei post (index.php eseguito)
/show.php           => Pagina che mostra il singolo post (show.php eseguito)

Con index.php come front controller
/index.php          => Pagina della lista dei post (index.php eseguito)
/index.php/show     => Pagina che mostra il singolo post (index.php eseguito)

Tip

La parte dell’URI index.php può essere rimossa se si usano le regole di riscrittura di Apache (o equivalente). In questo caso, l’URI risultante della pagina che mostra il post sarebbe semplicemente /show.

Usando un front controller, un singolo file PHP (index.php in questo caso) rende ogni richiesta. Per la pagina che mostra il post, /index.php/show eseguirà in effetti il file index.php, che ora è responsabile per gestire internamente le richieste, in base all’URI. Come vedremo, un front controller è uno strumento molto potente.

Creazione del front controller

Stiamo per fare un grosso passo avanti con l’applicazione. Con un solo file a gestire tutte le richieste, possiamo centralizzare cose come gestione della sicurezza, caricamento della configurazione, rotte. In questa applicazione, index.php deve essere abbastanza intelligente da rendere la lista dei post oppure il singolo post, in base all’URI richiesto:

<?php
// index.php

// carica e inizializza le librerie globali
require_once 'model.php';
require_once 'controllers.php';

// dirotta internamente la richiesta
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
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>Pagina non trovata</h1></body></html>';
}

Per una migliore organizzazione, entrambi i controllori (precedentemente index.php e show.php) sono ora funzioni PHP, entrambe spostate in un file separato, 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';
}

Come front controller, index.php ha assunto un nuovo ruolo, che include il caricamento delle librerie principali e la gestione delle rotte dell’applicazione, in modo che sia richiamato uno dei due controllori (le funzioni list_action() e show_action()). In realtà. il front controller inizia ad assomigliare molto al meccanismo con cui Symfony gestisce le richieste.

Tip

Un altro vantaggio di un front controller sono gli URL flessibili. Si noti che l’URL della pagina del singolo post può essere cambiato da /show a /read solo cambiando un unico punto del codice. Prima, occorreva rinominare un file. In Symfony, gli URL sono ancora più flessibili.

Finora, l’applicazione si è evoluta da un singolo file PHP a una struttura organizzata e che consente il riuso del codice. Dovremmo essere contenti, ma non ancora soddisfatti. Per esempio, il sistema delle rotte è instabile e non riconosce che la pagina della lista (/index.php) dovrebbe essere accessibile anche tramite / (con le regole di riscrittura di Apache). Inoltre, invece di sviluppare il blog, abbiamo speso diverso tempo sull‘“architettura” del codice (p.e. rotte, richiamo dei controllori, template, ecc.). Ulteriore tempo sarebbe necessario per gestire l’invio di form, la validazione dell’input, i log e la sicurezza. Perché dovremmo reinventare soluzioni a tutti questi problemi comuni?

Aggiungere un tocco di Symfony

Symfony alla riscossa! Prima di usare effettivamente Symfony, occorre accertarsi che PHP sappia come trovare le classi di Symfony. Possiamo farlo grazie all’autoloader fornito da Symfony. Un autoloader è uno strumento che rende possibile l’utilizzo di classi PHP senza includere esplicitamente il file che contiene la classe.

Nella cartella radice, creare un file composer.json con il seguente contenuto:

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

Quindi, scaricare Composer ed eseguire il seguente comando, che scaricherà Symfony in una cartella vendor/:

$ composer install

Oltre a scaricare le dipendenza, Composer genera un file vendor/autoload.php, che si occupa di auto-caricare tutti i file del framework Symfony, nonché dei file menzionati nella sezione autoload di composer.json.

Una delle idee principali della filosofia di Symfony è che il compito principale di un’applicazione sia quello di interpretare ogni richiesta e restituire una risposta. A tal fine, Symfony fornice sia una classe Symfony\Component\HttpFoundation\Request che una classe Symfony\Component\HttpFoundation\Response. Queste classi sono rappresentazioni orientate agli oggetti delle richieste grezze HTTP processate e delle risposte HTTP restituite. Usiamole per migliorare il nostro 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>Pagina non trovata</h1></body></html>';
    $response = new Response($html, Response::HTTP_NOT_FOUND);
}

// mostra gli header e invia la risposta
$response->send();

I controllori sono ora responsabili di restituire un oggetto Response. Per rendere le cose più facili, si può aggiungere una nuova funzione render_template(), che si comporta un po’ come il sistema di template di Symfony:

// 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);
}

// funzione aiutante per rendere i template
function render_template($path, array $args)
{
    extract($args);
    ob_start();
    require $path;
    $html = ob_get_clean();

    return $html;
}

Prendendo una piccola parte di Symfony, l’applicazione è diventata più flessibile e più affidabile. La classe Request fornisce un modo di accedere alle informazioni sulla richiesta HTTP. Nello specifico, il metodo getPathInfo() restituisce un URI più pulito (restituisce sempre /show e mai /index.php/show). In questo modo, anche se l’utente va su /index.php/show, l’applicazione è abbastanza intelligente per dirottare la richiesta a show_action().

L’oggetto Response dà flessibilità durante la costruzione della risposta HTTP, consentendo di aggiungere header e contenuti HTTP tramite un’interfaccia orientata agli oggetti. Mentre in questa applicazione le risposte molto semplici, tale flessibilità ripagherà quando l’applicazione cresce.

L’applicazione di esempio in Symfony

Il blog ha fatto molta strada, ma contiene ancora troppo codice per un’applicazione così semplice. Durante il cammino, abbiamo anche inventato un semplice sistema di rotte e un metodo che usa ob_start() e ob_get_clean() per rendere i template. Se, per qualche ragione, si avesse bisogno di continuare a costruire questo “framework” da zero, si potrebbero almeno utilizzare i componenti Routing e Templating, che già risolvono questi problemi.

Invece di risolvere nuovamente problemi comuni, si può lasciare a Symfony il compito di occuparsene. Ecco la stessa applicazione di esempio, ora costruita in Symfony:

// src/AppBundle/Controller/BlogController.php
namespace AppBundle\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('Blog/list.html.php', array('posts' => $posts));
    }

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

        if (!$post) {
            // mostra la pagina 404 page not found
            throw $this->createNotFoundException();
        }

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

I due controllori sono ancora leggeri. Ognuno usa la libreria ORM Doctrine per recuperare oggetti dalla base dati e il componente Templating per rendere un template e restituire un oggetto Response. Il template della lista è ora un po’ più semplice:

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

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

<h1>Lista dei post</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>

Il layout è quasi identico:

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

Note

Lasciamo il template di show come esercizio, visto che dovrebbe essere banale crearlo basandosi sul template della lista.

Quando il motore di Symfony (chiamato Kernel) parte, ha bisogno di una mappa che gli consenta di sapere quali controllori eseguire, in base alle informazioni della richiesta. Una configurazione delle rotte fornisce tali informazioni in un formato leggibile:

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

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

Ora che Symfony gestisce tutti i compiti più comuni, il front controller è semplicissimo. E siccome fa così poco, non si avrà mai bisogno di modificarlo una volta creato (e se si usa una distribuzione di Symfony, non servirà nemmeno 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();

L’unico compito del front controller è inizializzare il motore di Symfony (il Kernel) e passargli un oggetto Request da gestire. Il nucleo di Symfony quindi usa la mappa delle rotte per determinare quale controllore richiamare. Proprio come prima, il metodo controllore è responsabile di restituire l’oggetto Response finale. Non resta molto altro da fare.

Per una rappresentazione visuale di come Symfony gestisca ogni richiesta, si veda il diagramma di flusso della richiesta.

Dove consegna Symfony

Nei capitoli successivi, impareremo di più su come funziona ogni pezzo di Symfony e sull’organizzazione raccomandata di un progetto. Per ora, vediamo come migrare il blog da PHP puro a Symfony ci abbia migliorato la vita:

  • L’applicazione ora ha un codice organizzato chiaramente e coerentemente (sebbene Symfony non obblighi a farlo). Questo promuove la riusabilità e consente a nuovi sviluppatori di essere produttivi nel progetto in modo più rapido.
  • Il 100% del codice che si scrive è per la propria applicazione. Non occorre sviluppare o mantenere utilità a basso livello, come autoload, rotte o rendere i controllori.
  • Symfony dà accesso a strumenti open source, come Doctrine e i componenti Templating, Security, Form, Validation e Translation (solo per nominarne alcuni).
  • L’applicazione ora gode di URL pienamente flessibili, grazie al componente Routing.
  • L’architettura HTTP-centrica di Symfony dà accesso a strumenti potenti, come la cache HTTP fornita dalla cache HTTP interna di Symfony o a strumenti ancora più potenti, come Varnish. Questi aspetti sono coperti in un capitolo successivo, tutto dedicato alla cache.

Ma forse la parte migliore nell’usare Symfony è l’accesso all’intero insieme di strumenti open source di alta qualità sviluppati dalla comunità di Symfony! Si possono trovare dei buoni bundle su KnpBundles.com.

Template migliori

Se lo si vuole usare, Symfony ha un motore di template predefinito, chiamato Twig, che rende i template più veloci da scrivere e più facili da leggere. Questo vuol dire che l’applicazione di esempio può contenere ancora meno codice! Prendiamo per esempio il template della lista, scritto in Twig:

{# app/Resources/views/blog/list.html.twig #}
{% extends "layout.html.twig" %}

{% block title %}Lista dei post{% endblock %}

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

Il template corrispondente layout.html.twig è anche più facile da scrivere:

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

Twig è ben supportato in Symfony. Pur essendo sempre supportati i template PHP, continueremo a discutere dei molti vantaggi offerti da Twig. Per ulteriori informazioni, vedere il capitolo dei template.

Imparare di più con le ricette

  • /cookbook/templating/PHP
  • /cookbook/controller/service