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í:
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://movies-symfony/movie.php?id=34
/
, de manera que la part dinàmica de la ruta s'intercala
entre aqueixes barres. La URL anterior, en forma amigable, quedaria
així:
http://movies-symfony/movie/34
De pas, amb les rutes amigables se sol emmascarar el tipus d'arxiu 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 HomeController
{
/**
* @Route("/", name="home")
*/
public function home()
{
return new Response("Welcome to movies web site");
}
}
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://movies-symfony, 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;
HomeController
, 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;
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 comentari, en el qual es veu una anotació anomenada
Route
que està mapejant aqueix mètode amb una ruta.
/***
@Route("/", name="home")
*/
public function home() ...
home
, la qual cosa ens servirà per a fer que el
controlador siga independent de la ruta, com veurem després.
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
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à:
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
arxiu .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):
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 la fitxa d'una pel·lícula (el seu títol, el pòster i la sinopsi), a partir del seu codi, podríem plantejar una ruta com a http://movies-symfony/movies/54, i que el controlador ens mostrara la informació de la pel·lícula número 54. 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
MovieController
. Definirem un mètode anomenat show
, que mostrarà la
fitxa de la pel·lícula en el codi del qual li arribe en la ruta, encara que de
moment només mostrarà un missatge amb el codi de la pel·lícula indicat
en la ruta:
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class MovieController
{
/**
* @Route("/movies/{id}", name="movies_show")
*/
public function show(int $id)
{
return new Response("Movie data with id: $id");
}
}
id
de la pel·lícula 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 movies-symfony/movies/54
ens mostrarà
el missatge "Movie data with id: 54".
Millorarem un poc aquest controlador, per a simular que accedeix
a una font de dades per a obtenir la informació. Fins a veure en sessions
posteriors com connectar amb MySQL, anem a crear-nos el nostre propi array de
pel·lícules en la classe MovieController, i farem
que el mètode show
mostre les dades del contacte el codi del qual
s'haja indicat. El controlador quedarà així:
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class MovieController
{
private array $movies = [
["id"=>"2", "title" => "Ava", "tagline" => "Kill. Or be killed",
"release_date" => "2020-09-25"],
["id" => "3", "title" => "Bill &Ted Face the Music",
"tagline" => "The future awaits", "release_date" => "2020-09-24"],
["id" => "4", "title" => "Hard Kill",
"tagline" => "Take on a madman. Save the world.", "release_date" => "2020-09-14"],
["id" => "5", "title" => "The Owners", "tagline" => "",
"release_date" => "2020-05-10"],
["id" => "6", "title" => "The New Mutants",
"tagline" => "It's time to face your demons.", "release_date" => "2020-04-20"],
];
/**
* @Route("/movies/{id}", name="movies_show")
*/
public function show($id)
{
$result = array_filter($this->movies,
function($movie) use ($id)
{
return $movie["id"] == $id;
});
if (count($result) > 0)
{
$response = "";
$result = array_shift($result);
$response .= "<ul><li>" . $result["title"] . "</li>" .
"<li>" . $result["tagline"] . "</li>" .
"<li>" . $result["release_date"] . "</li></ul>";
return new Response("<html><body>$response</body></html>");
}
else
return new Response("Movie not found");
}
}
Com veiem, hem definit un array de pel·lícules, i dins del mètode filtrem
amb array_filter
aquell el codi del qual coincidisca amb l'indicat,
mostrant les seues dades en la resposta. Observa també com podem
incloure codi HTML en la mateixa resposta, encara que de seguida veurem
que hi ha formes més còmodes de fer açò.
Afegir requisits a les wildcards
Imaginem que volem definir un cercador de pel·lícules, de manera que li
passem com a part variable de la ruta una part del nom, i ens retorna
totes les pel·lícules que coincidisquen. Així, per a la "base de dades"
anterior, si accedírem a la ruta /movies/The
ens mostraria les
pel·lícules "The new mutants" i "The owners". Definim un segon mètode en
la nostra classe MovieController
per a processar aquesta ruta.
Quedaria així:
/**
* @Route("/movies/{text}", name="movies_filter")
*/
public function filter(string $text)
{
$result = array_filter($this->movies,
function($movie) use ($text)
{
return strpos($movie["title"], $text) !== false;
});
$response = "";
if (count($result) > 0)
{
foreach ($result as $movie) {
$response .= "<ul><li>" . $movie["title"] . "</li>" .
"<li>" . $movie["tagline"] . "</li>" .
"<li>" . $movie["release_date"] . "</li></ul>";
}
return new Response("<html><body>$response</body></html>");
}
else
return new Response("No movies found");
}
}
Si intentem llançar el URL anterior (http://movies-symfony/movies/The), obtindrem com a resultat "Movie not found", que és el missatge corresponent al controlador anterior (la fitxa de pel·lícula) quan no es trobava la pel·lícula amb el codi indicat. És a dir, s'ha llançat el controlador equivocat, i el motiu és simple, hem definit dues rutes a priori diferents:
/movies/{id}
per a la fitxa de la pel·lícula/movies/{text}
per a cercar pel·lícules pel títol
No obstant això, a efectes pràctics, ambdues rutes són el mateix: el
prefix /movies
seguit del que siga. En aquesta situació, Symfony
llança el primer dels controladors la ruta dels quals coincidisca amb la
indicada (el de la fitxa de pel·lícules, en aquest cas).
Per a diferenciar ambdues rutes, necessitem un criteri, i en aquest cas, el criteri serà que la fitxa de la pel·lícula necessita que el paràmetre id siga numèric. Açò s'especifica mitjançant la propietat requirements i una expressió regular, en definir la ruta de la fitxa:
@Route("/movies/{id}", name="movies_show", requirements={"id"="\d+"})
@Route("/movies/{id}<\d+>", name="movies_show")
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 fitxa de la pel·lícula
anterior, si volguérem que quan s'introduïsca la ruta /movies
(sense
id), es mostrara per defecte la pel·lícula amb codi 2, faríem açò:
/**
* @Route("/movies/{id}", name="movies_show", requirements={"id"="\d+"})
*/
public function show(int $id = 2)
encara que des de Symfony 4.1 també es pot especificar en la pròpia anotació, d'aquesta altra forma:
@Route("/movies/{id}<\d+>?2", name="movies_show")
Per a més informació podeu consultar la documentació oficial de Symfony: Routing