Come creare un semplice framework MVC con PHP

MVC design pattern

By on Tue Mar 30 in PHP, Programming


4.6
(14)

Introduzione

Oggi illustrerò come creare un semplice framework PHP seguendo il pattern MVC (Model-View-Controller). Alcuni anni fa ho tenuto un corso PHP in cui ho costruito un semplice e-commerce con gli studenti. Questo e-commerce era basato su un semplice framework MVC scritto in PHP. Le persone che hanno poi continuato con la programmazione avevano già un’infarinatura di ciò che significa MVC prima di mettere le mani su un vero framework.

I framework MVC sono ampiamente utilizzati nel settore perché offrono molti vantaggi per uno sviluppo rapido e strutturato. Ci sono framework MVC per la maggior parte dei linguaggi di programmazione che potreste conoscere, da DotNet a PHP. Sfortunatamente, questi framework potrebbero avere una curva di apprendimento ripida. Questo è dovuto al fatto che le persone devono imparare a scrivere codice nell’ecosistema del framework.

Nota personale sui framework PHP

Nel 2010 già sviluppavo software da più di 5 anni, e stavo cercando una buona soluzione per costruire un’applicazione web per il mio capo dell’epoca. Discutendo con un mio ex collega (grazie Davide C.!), ho iniziato ad usare Symfony 1.4. Ho usato l’ “approccio RTFM” (Read The Friendly Manual…) prima di scrivere qualsiasi parte. In due mesi ho realizzato un’applicazione di media complessità (registrazione, ACL, dashboard, frontend, ecc.).

In seguito, ho lavorato su Zend Framework, Symfony 2.0 e 5, e Laravel (attualmente lavorando sulla 5.8), e anche su microframework come Silex (non più mantenuto), Slim 3 e Lumen. Senza dubbio, il mio framework preferito è Laravel. Nonostante alcune “cose magiche” che possono spaventare le persone, Laravel offre un sacco di caratteristiche out-of-the-box che si possono semplicemente attivare con la giusta configurazione.

Cosa significa MVC?

MVC è un design pattern utilizzato per separare i dati (Model), le interfacce utente (View) e la logica dell’applicazione (Controller). Per essere in grado di seguire questo articolo, è necessario avere una buona conoscenza di PHP e OOP (Programmazione ad oggetti. Userò anche composer per gestire le l’autoloading e le dipendenze PHP.

Creare un semplice framework MVC con PHP

Indipendentemente dal fatto che stiate usando Docker, XAMPP, o qualsiasi altra cosa per il vostro ambiente di sviluppo, iniziamo a creare una semplice struttura per il nostro framework MVC. Per tutti i miei progetti uso una cartella chiamata “Solutions”, quindi se anche tu vuoi seguirmi, entra nella tua cartella Solutions, quindi crea una nuova cartella chiamata “simple-php-mvc” e poi entra in questa ultima cartella.

Creiamo le cartelle di base per il vostro MVC:

  1. app
  2. config
  3. public
  4. views
  5. routes

Iniziando in piccolo, creiamo i due file più importanti del nostro semplice MVC: index.php e htaccess.

Il file di configurazione htaccess

Entra nella cartella public, e creiamo un file chiamato index.php

Ora, al livello principale del tuo progetto, creiamo un nuovo file chiamato .htaccess

Aprilo con il tuo editor di testo preferito, e metti questo codice dentro il file htaccess:

<IfModule mod_rewrite.c>
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]
</IfModule>

Htaccess è un file di configurazione per il server web Apache, e la direttiva mod_rewrite dice ad Apache che ogni richiesta finirà alla index.php situata nella cartella chiamata public. Che cosa significa? Significa che se navighi su https://simple-php-mvc/page1, https://simple-php-mvc/page2 o https://simple-php-mvc/page3, tutte le richieste finiranno nella index.php sotto public, che è il punto di ingresso del tuo framework MVC. Questo è un grande vantaggio perché ora puoi gestire la richiesta in un solo posto, capire quale risorsa è stata richiesta e fornire la giusta risposta.
Un’altra cosa: usando htaccess e guidando il traffico sotto la cartella public, il resto della struttura del tuo progetto sarà nascosto a chiunque!

Ecco come appare il tuo progetto in questo momento:

PHP MVC application folder structure
Folder structure
app
config
public
    index.php
views
routes
.htaccess

Bootstrap del framework MVC

Ora hai bisogno di un modo per avviare la tua applicazione e caricare il codice di cui hai bisogno. Abbiamo già detto che index.php sotto la cartella public è il punto di ingresso, per questo motivo includiamo i file necessari da lì.

Prima di tutto, carichiamo il file di configurazione. Qui il contenuto del file index.php:

// Load Config
require_once '../config/config.php';

Ora possiamo creare un file config.php sotto la cartella config.

All’interno del file di configurazione, possiamo salvare le impostazioni del framework, per esempio, possiamo memorizzare il nome della nostra applicazione, il percorso della root, e, naturalmente, i parametri di connessione al database:

<?php
//site name
define('SITE_NAME', 'your-site-name');

//App Root
define('APP_ROOT', dirname(dirname(__FILE__)));
define('URL_ROOT', '/');
define('URL_SUBFOLDER', '');

//DB Params
define('DB_HOST', 'your-host');
define('DB_USER', 'your-username');
define('DB_PASS', 'your-password');
define('DB_NAME', 'your-db-name');

Nota: se hai deciso di mettere il framework in una sottocartella, apri /config/config.php e imposta il nome della sottocartella come valore della costante URL_SUBFOLDER.

Autolader

Vogliamo essere in grado di caricare le future classi senza alcun troppi sforzi, ovvero senza essere costretti a scrivere dozzine di include o require, quindi useremo l’autoloading PSR-4 con Composer.

È stato segnalato che alcuni hosting necessitano della direttiva classmap: classmap esaminerà ricorsivamente i file .php e .inc nelle directory e nei file specificati e rileverà le classi in essi contenute.
Composer è un gestore di dipendenze per PHP e permette di dichiarare le librerie da cui dipende il tuo progetto e lui le gestirà per te. Davvero utile!

Per prima cosa, a livello di root, dovete creare un file chiamato composer.json e aggiungere il seguente contenuto:

{
    "name": "gmaccario/simple-mvc-php-framework",
    "description": "Simple MVC PHP framework",
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "classmap": [
            "app/"
        ]
    }
}

Supponendo che abbiate già installato composer sul vostro computer o nel vostro container, eseguite il seguente comando a livello di root del vostro progetto, a seconda di dove state lavorando e dove è stato installato composer::

composer install

Se ora controlli la tua cartella principale, puoi vedere una nuova cartella chiamata vendor che contiene il file autoload.php e la cartella composer. Aprite il file index.php e aggiungete semplicemente il seguente codice all’inizio del file:

require_once '../vendor/autoload.php';

D’ora in poi, potete usare App come punto di partenza dei vostri namespace, in questo modo:

use App\Controllers\MyController;

Ora, impariamo cosa significa l’acronimo MVC.



Model

Un modello è un oggetto che rappresenta i vostri dati. Il modello sarà forgiato sulla struttura della vostra tabella di database e interagirà con le operazioni del database (creare, leggere, aggiornare e cancellare).

Per esempio, avete una tabella Prodotti come questa:

CREATE TABLE IF NOT EXISTS products (
  id int(10) NOT NULL auto_increment,
  title varchar(255) collate utf8_unicode_ci NOT NULL,
  description text collate utf8_unicode_ci,
  price decimal(12,5) NOT NULL,
  sku varchar(255) collate utf8_unicode_ci NOT NULL,
  image varchar(255) collate utf8_unicode_ci NOT NULL,
  PRIMARY KEY  (id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;

Prima di tutto, creiamo una nuova cartella chiamata Models dentro la cartella app. Poi creiamo un nuovo file chiamato Product sotto Models.

NOTA: L'obiettivo di questo articolo è spiegare come implementare un semplice MVC utilizzando PHP, non spiegare i principi SOLID. Quindi, in questo contesto, mescolerò il contesto del modello con l'accesso al database. Questo si avvicina al pattern Active Record, anche se si possono usare alternative come il pattern Data Mapper. 
Il modello Active Record in breve:
- Ogni istanza della classe rappresenta una singola riga nel database.
- Metodi come save, findBy, update e delete interagiscono direttamente con il database.

Il tuo modello di prodotto sarà:

<?php 
namespace App\Models;

class Product
{
    protected $id;
    protected $title;
    protected $description;
    protected $price;
    protected $sku;
    protected $image;

    // GET METHODS
    public function getId()
    {
        return $this->id;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function getDescription()
    {
        return $this->description;
    }

    public function getPrice()
    {
        return $this->price;
    }

    public function getSku()
    {
        return $this->sku;
    }

    public function getImage()
    {
        return $this->image;
    }

    // SET METHODS
    public function setTitle(string $title)
    {
        $this->title = $title;
    }

    public function setDescription(string $description)
    {
        $this->description = $description;
    }

    public function setPrice(string $price)
    {
        $this->price = $price;
    }

    public function setSku(string $sku)
    {
        $this->sku = $sku;
    }

    public function setImage(string $image)
    {
        $this->image = $image;
    }

    // CRUD OPERATIONS
    public function create(array $data)
    {

    }

    public function read(int $id)
    {

    }

    public function update(int $id, array $data)
    {

    }

    public function delete(int $id)
    {

    }
}

E questo è tutto. Con i metodi, potrete lavorare con gli oggetti di cui avete bisogno, popolando gli attributi necessari dell’oggetto con i valori reali basati sul modello.

View

La vista è responsabile di prendere i dati dal controllor e mostrare quei valori. Questo è tutto. Ci sono molti motori di template per PHP, da Twig a Blade. Per questo tutorial, useremo solo del semplice HTML per rendere le cose semplici.

Per creare una nuova vista, dobbiamo creare un nuovo file chiamato product.php sotto la cartella views. Sulla base degli attributi del prodotto, possiamo scrivere un semplice HTML come questo:

<!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>My Product:</h1>
        <ul>
            <li><?php echo $product->getTitle(); ?></li>
            <li><?php echo $product->getDescription(); ?></li>
            <li><?php echo $product->getPrice(); ?></li>
            <li><?php echo $product->getSku(); ?></li>
            <li><?php echo $product->getImage(); ?></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 vista è ora pronta per ricevere l’oggetto $prodotto e visualizzare i suoi valori.

Controller

Il controller è il cuore della logica dell’applicazione. È responsabile di accettare l’input e convertirlo in comandi per il modello o la vista. Creiamo una nuova cartella chiamata Controllers sotto la cartella app, e creiamo un nuovo file controller chiamato ProductController.php. Qui il contenuto:

<?php 

namespace App\Controllers;

use App\Models\Product;
use Symfony\Component\Routing\RouteCollection;

class ProductController
{
    // Show the product attributes based on the id.
    public function showAction(int $id, RouteCollection $routes)
    {
        $product = new Product();
        $product->read($id);

        require_once APP_ROOT . '/views/product.php';
    }
}

Molto semplice, vero? Ovviamente, le cose potrebbero essere più complesse, possiamo creare una classe controllor padre contenente metodi generici, un metodo di gestione vista e altre funzioni di aiuto. Ma per ora è sufficiente.

Il sistema di rotte

Ora abbiamo bisogno di un meccanismo per gestire gli URL. Vogliamo usare friendly URLs (URLs amichevoli); in altre parole, vogliamo avere a che fare con indirizzi web che siano facili da leggere e che includano parole che descrivano il contenuto della pagina web. Abbiamo quindi bisogno di un sistema di routing.

Possiamo creare il nostro personale sistema di rotte, oppure, dato che abbiamo usato composer per l’autoload, possiamo cercare nei pacchetti dell’ampio ecosistema Symfony e lavorare in modo intelligente!
Quindi, vediamo come possiamo trarre vantaggio dal componente Routing di Symfony. Qui la documentazione:

https://symfony.com/doc/current/create_framework/routing.html

Prima di tutto, installare il componente:

composer require symfony/routing
Install symfony/routing via composer.
Installare symfony/routing usando composer.

Se ora controllate all’interno della cartella vendor, potete vedere che è stata creata una nuova cartella chiamata symfony.

Cominciamo allora ad implementare il sistema di routing. L’obiettivo è quello di visualizzare i valori del prodotto con ID=1 quando si naviga sull’URL /prodotto/1
Creiamo un nuovo file chiamato web.php sotto la cartella routes. Questo file conterrà tutte le rotte della vostra applicazione.

<?php 

use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

// Routes system
$routes = new RouteCollection();
$routes->add('product', new Route(constant('URL_SUBFOLDER') . '/product/{id}', array('controller' => 'ProductController', 'method'=>'showAction'), array('id' => '[0-9]+')));

Usiamo le classi Route e RouteCollection del componente Routing di Symfony per creare ed elencare tutte le rotte di cui abbiamo bisogno. Iniziamo con una singola pagina di prodotto.

Poi, creiamo il motore di routing. Aggiungi un nuovo file chiamato Router.php all’interno della cartella della tua app e metti 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 codice è semplice e parla da solo, ma spieghiamolo un po’: l’URL matcher prende l’URI della richiesta e controlla se c’è una corrispondenza con le rotte definite in routes/web.php. Se c’è una corrispondenza, la funzione call_user_func_array farà la magia, chiamando il metodo giusto del controller giusto.
Inoltre, abbiamo usato la funzione array_walk per fare il cast dei valori numerici in valori interi, perché nei nostri metodi abbiamo usato la dichiarazione esplicita del tipo.

Querystring opzionale

Per via di queste righe di codice:

$arrayUri = explode('?', $_SERVER['REQUEST_URI']);
$matcher = $matcher->match($arrayUri[0]);

nei nostri controllers possiamo facilmente ottenere i valori dei parametri opzionali, in questo modo:

URL:

http://localhost/product/1?value1=true&value2=false

Controller:

var_dump($_GET['value1']);
var_dump($_GET['value2']);

Di solito, si utilizzano i valori inclusi nelle rotte, ma in alcuni casi questa soluzione può essere utile, ad esempio quando si hanno molti parametri a causa dei filtri.

Ora possiamo includere il sistema di rotte nel file index.php:

<?php

// Autoloader
require_once '../vendor/autoload.php';

// Load Config
require_once '../config/config.php';

// Routes
require_once '../routes/web.php';
require_once '../app/Router.php';

Ora che abbiamo preparato il sistema di routing, possiamo navigare nella pagina /product/1 e vedere il risultato. Ovviamente i valori ora sono vuoti. Aggiungiamo alcuni valori fake al prodotto (dentro il file ProductController.php):

public function read(int $id)
{
    $this->title = 'My first Product';
    $this->description = 'Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum ';
    $this->price = 2.56;
    $this->sku = 'MVC-SP-PHP-01';
    $this->image = 'https://via.placeholder.com/150';

    return $this;
}

E naviga di nuovo la pagina /product/1

Ora puoi aggiungere la tua connessione al database e restituire i valori dal database, usando una query grezza o un ORM come Doctrine o Eloquent.

Note aggiuntive sull’instradamento: le tue rotte potrebbero sovrapporsi l’una all’altra, per esempio, se scrivi /{pageSlug} prima di qualsiasi altra rotta, come /register. Puoi facilmente superare questo problema semplicemente scrivendo la tua rotta generale /{pageSlug} alla fine di tutte le rotte. Questa rotta diventerà il tuo fallback. Oppure, un’altra soluzione è aggiungere un prefisso, come /static/{pageSlug} o /public/{pageSlug}.

La homepage

Prepariamo ora la rotta della homepage. Aprire il route/web.php e aggiungere la nuova rotta:

$routes->add('homepage', new Route(constant('URL_SUBFOLDER') . '/', array('controller' => 'PageController', 'method'=>'indexAction'), array()));

Ovviamente, dobbiamo creare il nuovo controllor PageController:

<?php 

namespace App\Controllers;

use App\Models\Product;
use Symfony\Component\Routing\RouteCollection;

class PageController
{
        // Homepage action
	public function indexAction(RouteCollection $routes)
	{
            $routeToProduct = str_replace('{id}', 1, $routes->get('product')->getPath());

             require_once APP_ROOT . '/views/home.php';
	}
}

E anche la nuova vista:

<!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>

Dato che usiamo lo stesso codice per l’intestazione e il piè di pagina, possiamo creare una cartella di layout e separare il codice per questi pezzi di HTML.

Per navigare la pagina, aprite il vostro browser e navigate su questo URL:

http://localhost/

L’URL esatto dipende dalle vostre impostazioni. Io uso Docker per il mio ambiente locale, e di solito imposto la porta, quindi il mio URL potrebbe essere leggermente diverso dal vostro, per esempio, io uso

http://localhost:8887/

Nota: se hai installato il tuo progetto in una sottocartella come molti utenti sembrano fare, devi impostare la costante URL_SUBFOLDER nel file di configurazione. Per esempio, se hai installato questo progetto in una sottocartella chiamata simple-mvc-php-framework, il tuo URL finale sarà:

http://localhost/simple-mvc-php-framework/

Migliorare il framework MVC

Connessione al database e ORM, sessioni, cookies, un migliore controller per le pagine che accetti diversi parametri, o qualsiasi altra caratteristica può essere aggiunta molto facilmente, ma questo articolo vuole mostrare solo il modo di costruire un semplice framework MVC.

Scarica il codice

Puoi scaricare il file zip, o clonare il codice di questo articolo tramite Github. https://github.com/gmaccario/simple-mvc-php-framework

Conclusione

In questo articolo abbiamo visto come creare un semplice framework PHP seguendo il pattern MVC. Una volta che hai preso confidenza con il paradigma MVC, ti suggerisco di leggere la documentazione di Laravel (il mio framework MVC PHP preferito) o Symfony e iniziare a sperimentare! Noterai che lo sviluppo diventa più veloce rispetto ad usare una soluzione PHP pura e scritta da zero.

Grazie per il vostro tempo, spero che abbiate ottenuto nuove informazioni sul paradigma MVC. Fammi sapere cosa pensi di questo articolo attraverso i commenti o mandami un messaggio!

How useful was this post?

Click on a star to rate it!

Average rating 4.6 / 5. Vote count: 14

No votes so far! Be the first to rate this post.