MVC è un design pattern che separa i dati (Model), le interfacce utente (View) e la logica dell'applicazione (Controller). Per seguire questo articolo, è utile avere una solida conoscenza di PHP e della programmazione orientata agli oggetti (OOP). Utilizzeremo anche Composer per gestire il caricamento automatico di classi e dipendenze PHP.
Realizzare il framework MVC
Nel corso di questa guida è stato utilizzato il web server Laragon ma questa scelta non è vincolante. Inizieremo realizzando una struttura di partenza per il nostro framework che chiameremo Veranius. Ora create una directory che chiamerete Veranius
ed all'interno della stessa le altre cartelle alla base del progetto:
app
config
public
views
routes
Il file htaccess
Nella cartella public
create il file index.php
mentre nella root principale create il file .htaccess
per la riscrittura degli URL ed inserite in esso:
RewriteEngine On
# Stop processing if already in the /public directory
RewriteRule ^public/ - [L]
# Static resources if they exist
RewriteCond %{DOCUMENT_ROOT}/public/$1 -f
RewriteRule (.+) public/$1 [L]
# Route all other requests
RewriteRule (.*) public/index.php?route=$1 [L,QSA]
Utilizzando la direttiva mod_rewrite
ogni richiesta verso index.php
viene instradata nella cartella public
che funge da punto di ingresso per il framework. Questo è vantaggioso perché consente di gestire tutte le richieste in un unico punto, comprendere quale risorsa è stata richiesta e fornire la risposta appropriata. Utilizzando .htaccess
per dirigere il traffico verso public
il resto della struttura rimarrà nascosto dall'esterno.Ecco come appare la struttura del vostro progetto in questo momento:
Bootstrap del framework
Il primo passo consiste nel caricare il file di configurazione. Ecco il contenuto di index.php
:
<?php
require_once __DIR__.'/../config/config.php';
Ora si deve creare il file config.php
all'interno di config
e salvare le impostazioni del framework: nome dell'applicazione, percorso della root e parametri di connessione al database:
<?php
define('SITE_NAME', 'tutorial-site');
//App Root
define('APP_ROOT', dirname(dirname(__FILE__)));
define('URL_ROOT', '/');
define('URL_SUBFOLDER', '');
//DB Params
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'tutorial_site_db');
Nota: utilizza la costante
URL_SUBFOLDER
associata al nome della cartella nel caso in cui avessi inserito il progetto in una sottocartella.
Creare il database
Aprite la console del Web server e create il database digitando:
mysql -u root -p
Una volta loggati si può creare il database digitando:
CREATE DATABASE tutorial_site_db;
Autoloader
Per semplificare il caricamento delle future classi senza dover scrivere molte istruzioni include
o require
, utilizzeremo l'autoloading PSR-4 con Composer.
Alcuni hosting richiedono l'utilizzo della direttiva classmap
. Qiest'ultima esaminerà in modo ricorsivo i file .php
e .inc
nelle directory specificate e rileverà le classi presenti in esse. Composer è un gestore di dipendenze per PHP che consente di dichiarare le librerie da cui dipende un progetto e si occupa di gestirle. Se non lo avete scaricatevelo ed installatelo. Scegliete quello adatto al vostro sistema operativo da qui.
Per iniziare, create un file chiamato composer.json
nella radice del progetto e aggiungete il seguente contenuto:
{
"name": "veranius/tutorial-site",
"description": "Simple MVC PHP framework",
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"app/"
]
}
}
Ora, dal terminale, digitate:
composer install
Assicuratevi di essere nella radice principale del progetto. Il comando installerà le dipendenze elencate nel file composer.json
e aggiungerà la cartella vendor
contenente le dipendenze scaricate (se ce ne sono). Aprendola troverete il file autoload.php
e la cartella composer
.
Aprite il file index.php
ed aggiungete quanto di seguito così che l'autoload abbia effetto:
require_once __DIR__ .'/../vendor/autoload.php';
Il pattern MVC
Come anticipato, MVC è un design pattern che separa la logica di business (Model), l'interfaccia utente (View) e il controllo delle interazioni (Controller). Questa separazione favorisce la modularità, la manutenibilità e la scalabilità delle applicazioni.
Il Model
Il Model in MVC rappresenta la logica di business e i dati dell'applicazione. È responsabile della gestione e manipolazione dei dati, delle regole di business, delle operazioni di accesso al database e delle operazioni logiche sull'applicazione. In sintesi, il Model rappresenta il nucleo dell'applicazione che elabora e gestisce i dati senza preoccuparsi dell'interfaccia utente o delle interazioni esterne.
Create ora una semplice tabella users
all'interno del vostro database:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Potete quindi realizzare un primo modello ad essa correlato. Nella cartella app
creatr una sottocartella Models
e al suo interno la classe User
. Aprite il file della classe User
e inserite il codice seguente:
<?php
namespace App\Models;
class User
{
protected $id;
protected $username;
protected $email;
protected $password_hash;
protected $full_name;
protected $created_at;
// GET METHODS
public function getId()
{
return $this->id;
}
public function getUsername()
{
return $this->username;
}
public function getEmail()
{
return $this->email;
}
public function getPassword()
{
return $this->password_hash;
}
public function getName()
{
return $this->full_name;
}
public function getCreated()
{
return $this->created_at;
}
// SET METHODS
public function setUsername(string $username)
{
$this->username = $username;
}
public function setEmail(string $email)
{
$this->email = $email;
}
public function setPassword(string $password)
{
$this->password_hash = $password;
}
// CRUD OPERATIONS
public function create(array $data)
{
}
public function read(int $id)
{
}
public function update(int $id, array $data)
{
}
public function delete(int $id)
{
}
}
Utilizzando i metodi appropriati potete manipolare gli oggetti secondo le vostre esigenze, popolando gli attributi con i valori reali basati sul modello dati.
Le View
La vista ha il compito di ricevere i dati dal controller e visualizzarli. Questo è tutto! Esistono diversi motori di template per PHP, come Twig e Blade. Tuttavia per semplicità useremo HTML. Per creare una nuova vista è necessario un nuovo file chiamato users.php
in views
. Basandoci sugli attributi di User
possiamo scrivere del codice HTML come segue:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="favicon.png">
<title>Simple PHP MVC</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<section>
<h1>User List:</h1>
<ul>
<li><?php echo $user->getUsername(); ?></li>
<li><?php echo $user->getEmail(); ?></li>
<li><?php echo $user->getName(); ?></li>
</ul>
<a href="<?php echo $routes->get('homepage')->getPath(); ?>">Back to homepage</a>
<section>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
La view potrà ora ricevere l'oggetto User
e visualizzarne i valori.
Il Controller
Il controller è il cuore della logica dell'applicazione, responsabile di accettare l'input e convertirlo in comandi per il modello o la vista. Per creare un nuovo controller generate una cartella chiamata Controllers
sotto la directory app
e successivamente un nuovo file UserController.php
. Di seguito viene riportato il contenuto del file:
<?php
namespace App\Controllers;
use App\Models\User;
use Symfony\Component\Routing\RouteCollection;
class UserController
{
// Show the product attributes based on the id.
public function showAction(int $id, RouteCollection $routes)
{
$user= new User();
$user->read($id);
require_once APP_ROOT . '/views/user.php';
}
}
Semplice vero? A dire il vero potremmo anche creare una classe controller base contenente dei metodi generici, un gestore di viste ed altre funzioni di supporto. Al momento però direi di mantenere la semplicità che per iniziare è più che sufficiente.
Il sistema di rotte
Ora è necessario gestire gli URL in modo efficiente. Meglio utilizzare friendly URL, cioè indirizzi web facili da leggere, che descrivono il contenuto della pagina. Per questo, avete bisogno di un sistema di routing.
Potete sviluppare un sistema di routing personalizzato ma dato che avete già utilizzato Composer per l'autoload, potete sfruttare i pacchetti disponibili e lavorare in modo più efficiente. Per prima cosa installiamo il componente usando Composer:
composer require symfony/routing
Se controllate, all'interno di vendor
è stata creata un'altra cartella denominata Simphony
. Iniziate ad implementare il sistema di routing per visualizzare i dettagli dell'utente con ID=1 quando si accede all'URL /user/1
. Create un nuovo file chiamato web.php
nella cartella routes
. Questo file conterrà tutte le route dell'applicazione:
<?php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
// Routes system
$routes = new RouteCollection();
$routes->add('user', new Route(constant('URL_SUBFOLDER') . '/user/{id}', array('controller' => 'UserController', 'method'=>'showAction'), array('id' => '[0-9]+')));
Utilizzate le classi Route
e RouteCollection
del componente Routing di Symfony per definire ed elencare tutte le route necessarie. Iniziate con una singola pagina utente.
Il motore di routing
Per creare il motore di routing aggiungete un nuovo file chiamato Router.php
in app e inserite questo codice al suo interno:
<?php
namespace App;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
class Router
{
public function __invoke(RouteCollection $routes)
{
$context = new RequestContext();
// Routing can match routes with incoming requests
$matcher = new UrlMatcher($routes, $context);
try {
$arrayUri = explode('?', $_SERVER['REQUEST_URI']);
$matcher = $matcher->match($arrayUri[0]);
// Cast params to int if numeric
array_walk($matcher, function(&$param)
{
if(is_numeric($param))
{
$param = (int) $param;
}
});
// https://github.com/gmaccario/simple-mvc-php-framework/issues/2
// Issue #2: Fix Non-static method ... should not be called statically
$className = '\\App\\Controllers\\' . $matcher['controller'];
$classInstance = new $className();
// Add routes as paramaters to the next class
$params = array_merge(array_slice($matcher, 2, -1), array('routes' => $routes));
call_user_func_array(array($classInstance, $matcher['method']), $params);
} catch (MethodNotAllowedException $e) {
echo 'Route method is not allowed.';
} catch (ResourceNotFoundException $e) {
echo 'Route does not exists.';
} catch (NoConfigurationException $e) {
echo 'Configuration does not exists.';
}
}
}
// Invoke
$router = new Router();
$router($routes);
Il componente di routing usa l'URL matcher per confrontare l'URI della richiesta con le rotte definite nel file routes/web.php
. Se viene trovata una corrispondenza, la funzione call_user_func_array
viene utilizzata per chiamare il metodo appropriato nel controller corrispondente.
La funzione array_walk
converte i valori numerici in valori interi, poiché è stato dichiarato esplicitamente il tipo di dato nei metodi. Questo assicura la corretta gestione dei parametri passati ai metodi dei controller.
La querystring opzionale
Grazie alle seguenti righe di codice:
$arrayUri = explode('?', $_SERVER['REQUEST_URI']);
$matcher = $matcher->match($arrayUri[0]);
nel controller siete in grado di ottenere valori di parametri opzionali nel modo seguente:
http://localhost/product/1?value1=true&value2=false
Includete il sistema di rotte nel file index.php
:
require_once __DIR__ .'/../vendor/autoload.php';
require_once __DIR__.'/../config/config.php';
// Routes
require_once '../routes/web.php';
require_once '../app/Router.php';
Ora che avete configurato il sistema di routing, potete navigare sulla pagina /user/1
e visualizzare il risultato. Tuttavia, attualmente i valori sono vuoti. Per aggiungere dati di esempio all'utente, modificate read
nel model User.php
come segue:
public function read(int $id)
{
$this->username= 'lucio_ticali';
$this->email = 'ticalilucio@gmail.com';
$this->full_name = 'Luciano Ticali';
return $this;
}
Ora è possibile stabilire una connessione al database e recuperare i valori da esso utilizzando una query SQL o un ORM come Doctrine o Eloquent. Questo consentirà di mostrare dati reali e dinamici sulla pagina /user/1
anziché valori statici o vuoti.
Per quanto riguarda l'istruzione delle rotte, è importante considerare la loro sequenza poiché possono sovrapporsi tra loro. Ad esempio, definendo una rotta generale come /{pageSlug}
prima di qualsiasi altra rotta specifica, come /register
, potreste incontrare conflitti nell'instradamento delle richieste. Una soluzione pratica a questo problema è posizionare la rotta generale /{pageSlug}
alla fine della lista delle rotte, in modo che diventi il fallback quando non viene trovata alcuna corrispondenza specifica. Altrimenti, potete anche aggiungere un prefisso distintivo alle rotte generiche, come /static/{pageSlug}
o /public/{pageSlug}
, per evitare sovrapposizioni indesiderate. Queste strategie aiutano a mantenere un instradamento ordinato e coerente nel sistema.
La Home
Aprite il file route/web.php
ed aggiungete la nuova rotta relativa alla home del sito:
$routes->add('homepage', new Route(constant('URL_SUBFOLDER') . '/', array('controller' => 'PageController', 'method'=>'indexAction'), array()));
Quindi create il controller PageController
all'interno della folder Controller
:
<?php
namespace App\Controllers;
use App\Models\User;
use Symfony\Component\Routing\RouteCollection;
class PageController {
// Homepage action
public function indexAction(RouteCollection $routes) {
$routeToProduct = str_replace('{id}', 1, $routes->get('user')->getPath()); require_once APP_ROOT . '/views/home.php';
}
}
ed anche la view home
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="favicon.png">
<title>Simple PHP MVC</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<section>
<h1>Homepage</h1>
<p><a href="<?php echo $routeToProduct ?>">Check the first product</a>
</p><section>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
Conclusione
Nel nostro viaggio attraverso lo sviluppo di un semplice framework PHP basato sul pattern MVC, abbiamo compreso l'importanza di una struttura ben organizzata per gestire le diverse componenti di un'applicazione Web. È importante notare che questo esempio è solo un punto di partenza, un vero framework MVC necessiterebbe di funzionalità aggiuntive per gestire aspetti come cookie, sessioni e Object-Relational Mapping.
Per esempio, la gestione dei cookies e delle sessioni è essenziale per mantenere lo stato dell'utente tra le varie richieste HTTP. Si possono utilizzare funzioni native di PHP come setcookie()
per i cookies e $_SESSION
per le sessioni. O adottare librerie o componenti dei principali framework che semplificano queste operazioni.
Inoltre, l'utilizzo di un ORM come Eloquent in Laravel, o Doctrine in Symfony, semplifica l'interazione con il database. Consentendo di manipolare i dati tramite oggetti e query orientate agli oggetti anziché query SQL. Questo rende il codice più leggibile e manutenibile, offre inoltre funzionalità avanzate come le relazioni tra entità e la gestione automatica delle transazioni.
Grazie per aver dedicato del tempo a leggere questo articolo. Spero che abbia fornito nuove prospettive sulle pratiche di sviluppo e sulle potenzialità offerte dal pattern MVC.