Salta el contingut

El patró MVC en Symfony

El patró MVC

MVC són les sigles de Model-Vista-Controlador (o en anglès, Model-View-Controller), i és el patró d'arquitectura de programari per excel·lència ara mateix en el món de les aplicacions web, i fins i tot de moltes aplicacions d'escriptori.

Com el seu nom indica, aquest patró es basa a dividir el disseny o l'estructura d'una aplicació web en tres components fonamentals:

  • El model, que podríem resumir com el conjunt de totes les dades o informació que maneja l'aplicació. Típicament seran variables o objectes extrets d'una base de dades o qualsevol altre sistema d'emmagatzematge, per la qual cosa el codi del model normalment estarà format per instruccions per a connectar amb la base de dades, recuperar informació d'ella i emmagatzemar-la en algunes variables determinades. Per tant, no tindrà coneixement de la resta de components del sistema.
  • La vista, que és l'intermediari entre l'aplicació i l'usuari, és a dir, la qual cosa l'usuari veu en pantalla de l'aplicació. Per tant, la vista la compondran les diferents pàgines, formularis, etc, que l'aplicació mostrarà a l'usuari per a interactuar amb ell.
  • El controlador (o controladors), que són els fragments de codi encarregats de coordinar el funcionament general de l'aplicació. Davant peticions dels usuaris, les arrepleguen, les identifiquen, i accedeixen al model per a actualitzar o recuperar dades, i al seu torn, decideixen què vestisca mostrar-li a l'usuari a continuació de l'acció que acaba de realitzar.

És un patró arquitectònic disseny molt concís i ben estructurat, la qual cosa li ha valgut la fama que té avui dia. Entre els seus molts avantatges, permet aïllar el codi dels tres elements involucrats (vista, model i controlador), de manera que el treball és molt més modular i divisible, podent encarregar-se de les vistes, per exemple, un dissenyador web que no tinga molta idea de programació en el servidor, i del controlador un programador PHP que no tinga moltes nocions d'HTML.

En forma d'esquema, podríem representar-ho així:

Components del framework MVC

Components del framework MVC

Les peticions de l'usuari arriben al controlador, que les identifica, i es comunica amb el model per a obtenir les dades necessàries, i amb les vistes per a decidir què mostrar a continuació i omplir-la amb les dades del model, per a després servir-li-la a l'usuari com a resposta. En aquesta sessió veurem com definir controladors en Symfony, i associar-los a rutes, de manera que mostren algun contingut o vista com a resposta.

Controladors i rutes en Symfony

Per a crear pàgines en una aplicació Symfony es necessiten dos elements: una ruta (és a dir, un URI que indique a quin contingut accedir de la web) i un controlador associat a ella, que serà l'encarregat de mostrar el resultat (la pàgina) per a aqueixa petició de ruta.

El concepte de ruta en Symfony

Les rutes en Symfony poden ser tradicionals o amigables. Una ruta tradicional és aquella la part dinàmica de la qual s'especifica en la query string. Per exemple, si volem saber la fitxa d'una pel·lícula a través del seu codi, tindríem un URL com aquest:

http://blog-site/post.php?id=1
Les rutes amigables són aquelles que separen els seus elements únicament per barres /, de manera que la part dinàmica de la ruta s'intercala entre aqueixes barres. La URL anterior, en forma amigable, podria quedar així:

http://blog-site/post/1

o millor encara:

http://blog-site/post/hello-world

A més, amb les rutes amigables es sol emmascarar el tipus de fitxer que s'està sol·licitant, amb el que s'omet la informació de si és una pàgina PHP, o HTML, o de qualsevol altre tipus. Ens centrarem en aquestes altres rutes en aquest curs.

El nostre primer controlador

Un controlador en Symfony és bàsicament una classe que conté mètodes PHP l'únic propòsit dels quals és obtenir la petició de l'usuari per a una ruta concreta, processar-la i enviar-li una resposta.

Els controladors se situen dins de la carpeta src del nostre projecte Symfony, concretament en l'espai de noms Controller que ja està creat.

namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController
{

    #[Route('/', name: 'app_default')]
    public function home()
    {
        return new Response("Welcome to my blog");
    }
}

Observa el codi de la classe, i el del mètode home() en concret. Simplement mostra un missatge de benvinguda a través d'un objecte Response, que s'empra per a definir la resposta a enviar. Si guardem els canvis i accedim a http://blog-site, veurem aquest missatge de benvinguda.

Definir els espais de noms (namespaces)

Si dones una ullada al codi del controlador anterior, veuràs que comença amb la línia:

namespace App;
El que estem fent és situar la classe (DefaultController, en aquest cas) dins d'un espai de noms. Cada subcarpeta que hi ha dins de la carpeta src constitueix un espai de noms, de manera que quan situem un arxiu font dins d'un d'aqueixos subdirectoris, hem d'indicar que pertany a aquest espai de noms. Tots aquests espais de noms pengen d'una arrel App, per la qual cosa l'espai de noms per al nostre controlador és App (els subespais se separen amb barres invertides).

Els espais de noms són útils en aplicacions amb molts arxius font, com solen ser les aplicacions web més o menys importants, ja que es corre el risc de cridar exactament igual a dues classes que estiguen en carpetes diferents. Si les agrupem per espais de noms, no hi haurà problema a cridar igual a les classes i als arxius. El concepte és similar al de paquet (package) en llenguatges com Java. De fet, en Java tenim molts exemples de classes que es diuen igual i pertanyen a paquets diferents, amb el que són fàcilment diferenciables.

Incloure altres espais de noms en el fitxer actual

Tornem de nou al codi del controlador. Després de definir a quin espai de noms pertany aquest controlador (App), podem necessitar utilitzar objectes d'altres espais de noms. En aquest cas, per exemple, utilitzem un objecte Response que pertany a l'espai Symfony. Per a poder utilitzar aquest objecte de forma còmoda i no haver de col·locar tot aquest prefix cada vegada que vulguem fer referència al tipus Response, hem d'afegir una instrucció use indicant la ruta completa fins a la classe que utilitzarem:

use Symfony;
Després d'açò, ja podrem referenciar a la classe Response directament pel seu nom en qualsevol lloc d'aquest fitxer font. De la mateixa manera, podem afegir totes les sentències use que considerem, per a fer referència a tots els elements externs que necessitarem.

També podem definir un àlies per a nomenar a la classe incorporada dins del nostre arxiu font. Si, en lloc d'utilitzar Response, volem referenciar a aquest tipus amb una forma més abreujada (Res, per exemple), faríem açò:

use Symfony as Res;
 ...
  return new Res(...);

Crear controladors per línia de comandos

Mitjançant el comando bin/console podem crear un controlador, amb la instrucció:

php bin/console make:controller ControllerName

Es crearà automàticament un arxiu ControllerName.php en la carpeta src/Controller, i una plantilla associada al mateix, en la carpeta templates o alguna subcarpeta. Com a avantatge destacable d'aquesta forma de crear controladors, ens crea automàticament el namespace i afig els recursos externs (use) que necessitem. Però, com a desavantatge, ens crea una plantilla i unes connexions amb ella que normalment no necessitarem, i haurem de retocar.

Definint rutes

Donem una ullada més al codi del controlador que hem fet. Abans del mètode home hi ha un especie de comentari. Realment no és un comentari, es tracta d'un atribut, una forma d'afegir metadades al codi. En aquest exemple s'indica que aquest controlador s'activa en la ruta / i el seu nom és app_default.

#[Route('/', name: 'app_default')]
public function home() ...

Info

Els atributs van ser afegits a partir de PHP 8.0. En versions anteriors de PHP s'utilitzaven anotacions, que complien la mateixa funció:

/*** 
    @Route("/", name="home") 
*/
Val a dir que mentre els atributs són nadius en PHP, les anotacions no ho són. Les annotacions són una mena de constructe creat per poder afegir metadades adaptant les comentaris, com poden ser els docblocks.

Una altra forma de definir rutes: l'arxiu config/routes.yaml

Existeix una forma alternativa de definir rutes sense utilitzar anotacions, que consisteix a editar l'arxiu config/routes.yaml i afegir la nova ruta amb el controlador i nom associats. Per exemple, per al cas anterior, si volem que en accedir a l'arrel de l'aplicació s'active el mètode home del controlador HomeController, assignant-li a la ruta el nom home (tal com hem fet en l'exemple anterior), afegiríem aquestes línies al fitxer:

home: 
    path: /
    controller: App\Controller\HomeController::home
No obstant això, si atenem a la documentació oficial de Symfony, es recomana definir les rutes mitjançant anotacions, per la qual cosa d'ara endavant utilitzarem aquest mecanisme en les anotacions.

Comprovar les rutes de la nostra aplicació

Utilitzant la consola de Symfony (fitxer bin/console del nostre projecte) podem comprovar quines rutes hi ha actualment definides en la nostra aplicació, mitjançant aquest comando:

php bin/console debug:router

Mostrarà el llistat de rutes, indicant el seu nom, i la ruta associada. A més de la nostra ruta arrel, apareixeran altres rutes creades per defecte per a opcions de depuració i proves, com per exemple les rutes profiler per a rastrejar i obtenir detalls de les peticions realitzades. No entrarem en aqueixos detalls en aquest curs.

Configurar la reescriptura de rutes

Abans de continuar, hi ha un cosa que hem de tenir en compte: el controlador que hem provat (HomeController::home) funciona perquè fa referència a l'arrel de l'aplicació. Si canviem la ruta per qualsevol altra, com per exemple /home, no funcionarà:

Not found

El motiu és que encara no tenim configurat el nostre projecte perquè reescriga les rutes de forma amigable. Per a açò, hauríem de tenir un fitxer .htaccess en la carpeta public amb els paràmetres de configuració d'Apache per a aqueixa reescriptura. Com aqueixa tasca és una miqueta manual, l'eina composer posa a la nostra disposició un parell de comandos per a fer-ho per nosaltres.

Des de la carpeta del nostre projecte escriurem:

composer require symfony/apache-pack

En executar-lo ens mostrarà el següent missatge:

Symfony operations: 1 recipe (5a8f9ff66bdb49d40606adc556254e91)
  -  WARNING  symfony/apache-pack (>=1.0): From github.com/symfony/recipes-contrib:master
    The recipe for this package comes from the "contrib" repository, which is open to community contributions.
    Review the recipe at https://github.com/symfony/recipes-contrib/tree/master/symfony/apache-pack/1.0

    Do you want to execute this recipe?
    [y] Yes
    [n] No
    [a] Yes for all packages, only for the current installation session
    [p] Yes permanently, never ask again for this project
    (defaults to n): 
Al que respondrem y o p si volem que no ens torne a preguntar.

Les receptes de Symfony

Les receptes de Symfony permeten automatitzar la configuració de paquets Composer mitjançant el connector Symfony Flex Composer.

Hi ha receptes oficials i contribucions de tercers (contrib) és a dir, no són receptes dels desenvolupadors oficials de Symfony. Aquest és el cas de la recepta symfony/apache-pack, per la qual cosa ens pregunta si volem instal·lar-la igualment.

Podem executar composer recipes per veure les receptes que hi ha instal·lades i si cal actualitzar-les.

A partir d'aquest punt, ja podrem crear les rutes amigables que vulguem en el nostre projecte. Recorda repetir aquests comandos en tots els projectes Symfony que utilitzen rutes amigables.

Rutes amb paràmetres

Existeixen algunes rutes que tenen parts variables. Per exemple, si volguérem mostrar una entrada del bloc (el seu títol, la data i l'autor), a partir del seu codi, podríem plantejar una ruta com a http://blog-site/posts/1, i que el controlador ens mostrara la informació de l'entrada amb id 1. Però, si cridem a aqueixa ruta amb un altre codi, haurà de mostrar la informació de la pel·lícula amb l'altre codi.

Anem a crear un nou controlador en el nostre espai de noms Controller dins de la carpeta src. En aquest cas, cridarem a la classe DefaultController. Definirem un mètode anomenat show, que mostrarà l'entrada en el codi del qual li arribe en la ruta, encara que de moment només mostrarà un missatge amb el codi de l'entrada indicat en la ruta:

namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController
{

    #[Route("/posts/{id}", name: "posts_show")]
    public function show(int $id)
    {
        return new Response("Post with id: $id");
    }
}
Observa com en l'atribut afegim el id del post com un element variable, gràcies a les claus (a aquesta notació se li crida wildcard). El mètode associat a la ruta ha de tenir un paràmetre amb aqueixa mateixa dada (l'identificador), de manera que puguem utilitzar-lo dins del mètode. Si ara accedim a la URI blog-site/posts/54 ens mostrarà el missatge "Post with id: 54".

Afegir requisits a les wildcards

Tal com hem definit el wildcard què passaria si enlloc d'un enter rebera un text com a paràmetre?

Symfony ens permet afegir requisits als wildcards en forma d'expressió regular per poder afinar millor a l'hora de llegir els paràmetres.

Per exemple:

#[Route("/posts/{id}<\d+>", name: "posts_show")]

D'aquesta forma la ruta posts_show sols s'activarà si el paràmetre rebut és un dígit.

Afegir valors per defecte a les wildcards

En algunes ocasions, també ens pot interessar donar un valor per defecte a una wildcard perquè, si en la ruta no s'especifica res, tinga aquest valor per defecte. Açò s'aconsegueix assignant un valor per defecte al paràmetre associat en el controlador. En el cas de la publicació anterior, si volguérem que quan s'introduïsca la ruta /posts (sense id), es mostrara per defecte la publicació amb id 2, faríem açò:

#[Route('/posts/{id<\d+>?2}', name: 'posts_show')]

Per a més informació podeu consultar la documentació oficial de Symfony: Routing