Seguro que algunos que hayan leído o visto algunos de los tutoriales o ejemplos que pongo sobre programación en PHP con y sin frameworks, pueden no estar de acuerdo conmigo en ciertos detalles, o incluso estar pensando «este chico no está programando verdaderamente orientado a objetos» o «no sigue el paradigma a rajatabla» (todo lo que explico lo hago desde mi punto de vista actual, nunca digo que sea la verdad absoluta o lo más correcto siempre).
Pues bien hoy voy a poner un ejemplo muy bueno de como programar realmente orientado a objetos en PHP puro con MVC. Lo que voy a mostrar hoy perfectamente podría ser la base para construirnos un pequeño framework propio, veremos incluso como hacer un controlador frontal, como crear objetos que representen entidades de la base de datos, etc, por lo tanto lo que voy a enseñar hoy es un ejemplo muy didáctico y muy completo.
Estructura de directorios
En nuestro «framework» tendremos varios directorios:
- config: aquí irán los ficheros de configuración de la base de datos, globales, etc.
- controller: como sabemos en la arquitectura MVC los controladores se encargarán de recibir y filtrar datos que le llegan de las vistas, llamar a los modelos y pasar los datos de estos a las vistas. Pues en este directorio colocaremos los controladores
- core: aquí colocaremos las clases base de las que heredarán por ejemplo controladores y modelos, y también podríamos colocar más librerías hechas por nosotros o por terceros, esto sería el núcleo del framework.
- model: aquí irán los modelos, para ser fieles al paradigma orientado objetos tenemos que tener una clase por cada tabla o entidad de la base de datos(excepto para las tablas pivote) y estas clases servirán para crear objetos de ese tipo de entidad(por ejemplo crear un objeto usuario para crear un usuario en la BD). También tendremos modelos de consulta a la BD que contendrán consultas más complejas que estén relacionadas con una o varias entidades.
- view: aquí iran las vistas, es decir, donde se imprimirán los datos y lo que verá el usuario.
- index.php será el controlador frontal por el que pasará absolutamente todo en la aplicación.
Aprende todo de PHP, MVC y sus frameworks aquí: Master en PHP: Aprende PHP, SQL, POO, MVC, Laravel, Symfony 4, WordPress y más
Crear ficheros de configuración
En el directorio config, crearemos un fichero database.php en el que irá la configuración de la base de datos. Este devuelve un array que posteriormente utilizaremos.
<?php return array( "driver" =>"mysql", "host" =>"localhost", "user" =>"root", "pass" =>"", "database" =>"pruebas", "charset" =>"utf8" ); ?>
También podemos crearnos un fichero gobal.php en el que irán constantes que luego nos servirán por ejemplo para establecer controladores y acciones por defecto (y todo lo que queramos meterle).
<?php define("CONTROLADOR_DEFECTO", "Usuarios"); define("ACCION_DEFECTO", "index"); //Más constantes de configuración ?>
Crear las clases del núcleo
Primero crearemos la clase Conectar que nos servirá para conectarnos a la base de datos utilizando el driver MySQLi que es el más rápido aunque muchos por ahí recomienden PDO, y también nos servirá para conectar a la base de datos un constructor de consultas que he incluido en el proyecto llamado Fluent Query Builder (el que utiliza Laravel).
<?php class Conectar{ private $driver; private $host, $user, $pass, $database, $charset; public function __construct() { $db_cfg = require_once 'config/database.php'; $this->driver=$db_cfg["driver"]; $this->host=$db_cfg["host"]; $this->user=$db_cfg["user"]; $this->pass=$db_cfg["pass"]; $this->database=$db_cfg["database"]; $this->charset=$db_cfg["charset"]; } public function conexion(){ if($this->driver=="mysql" || $this->driver==null){ $con=new mysqli($this->host, $this->user, $this->pass, $this->database); $con->query("SET NAMES '".$this->charset."'"); } return $con; } public function startFluent(){ require_once "FluentPDO/FluentPDO.php"; if($this->driver=="mysql" || $this->driver==null){ $pdo = new PDO($this->driver.":dbname=".$this->database, $this->user, $this->pass); $fpdo = new FluentPDO($pdo); } return $fpdo; } } ?>
Seguimos creando el fichero EntidadBase.php de esta clase heredarán los modelos que representen entidades, en el constructor le pasaremos el nombre de la tabla y tendremos tantos métodos como queramos para ayudarnos con las peticiones a la BD a través de los objetos que iremos creando. Lo bueno que tiene es que estos métodos pueden ser reutilizados en otras clases ya que le indicamos la tabla en el constructor.
<?php class EntidadBase{ private $table; private $db; private $conectar; public function __construct($table) { $this->table=(string) $table; require_once 'Conectar.php'; $this->conectar=new Conectar(); $this->db=$this->conectar->conexion(); } public function getConetar(){ return $this->conectar; } public function db(){ return $this->db; } public function getAll(){ $query=$this->db->query("SELECT * FROM $this->table ORDER BY id DESC"); //Devolvemos el resultset en forma de array de objetos while ($row = $query->fetch_object()) { $resultSet[]=$row; } return $resultSet; } public function getById($id){ $query=$this->db->query("SELECT * FROM $this->table WHERE id=$id"); if($row = $query->fetch_object()) { $resultSet=$row; } return $resultSet; } public function getBy($column,$value){ $query=$this->db->query("SELECT * FROM $this->table WHERE $column='$value'"); while($row = $query->fetch_object()) { $resultSet[]=$row; } return $resultSet; } public function deleteById($id){ $query=$this->db->query("DELETE FROM $this->table WHERE id=$id"); return $query; } public function deleteBy($column,$value){ $query=$this->db->query("DELETE FROM $this->table WHERE $column='$value'"); return $query; } /* * Aquí podemos montarnos un montón de métodos que nos ayuden * a hacer operaciones con la base de datos de la entidad */ } ?>
Ahora crearemos la clase ModeloBase que heredará de la clase EntidadBase y a su vez será heredada por los modelos de consultas. La clase ModeloBase permitirá utilizar el constructor de consultas que hemos incluido y también los métodos de EntidadBase, así como otros métodos que programemos dentro de la clase, por ejemplo yo tengo un método para ejecutar consultas sql que directamente me devuelve el resultset en un array de objetos preparado para pasárselo a una vista, podríamos tener cientos para diferentes cosas.
<?php class ModeloBase extends EntidadBase{ private $table; private $fluent; public function __construct($table) { $this->table=(string) $table; parent::__construct($table); $this->fluent=$this->getConetar()->startFluent(); } public function fluent(){ return $this->fluent; } public function ejecutarSql($query){ $query=$this->db()->query($query); if($query==true){ if($query->num_rows>1){ while($row = $query->fetch_object()) { $resultSet[]=$row; } }elseif($query->num_rows==1){ if($row = $query->fetch_object()) { $resultSet=$row; } }else{ $resultSet=true; } }else{ $resultSet=false; } return $resultSet; } //Aqui podemos montarnos métodos para los modelos de consulta } ?>
La siguiente clase que crearemos es ControladoresBase de la cual heredarán los controladores, esta clase carga EntidadesBase, ModelosBase, y todos los modelos creados dentro del directorio model.
<?php class ControladorBase{ public function __construct() { require_once 'EntidadBase.php'; require_once 'ModeloBase.php'; //Incluir todos los modelos foreach(glob("model/*.php") as $file){ require_once $file; } } //Plugins y funcionalidades /* * Este método lo que hace es recibir los datos del controlador en forma de array * los recorre y crea una variable dinámica con el indice asociativo y le da el * valor que contiene dicha posición del array, luego carga los helpers para las * vistas y carga la vista que le llega como parámetro. En resumen un método para * renderizar vistas. */ public function view($vista,$datos){ foreach ($datos as $id_assoc => $valor) { ${$id_assoc}=$valor; } require_once 'core/AyudaVistas.php'; $helper=new AyudaVistas(); require_once 'view/'.$vista.'View.php'; } public function redirect($controlador=CONTROLADOR_DEFECTO,$accion=ACCION_DEFECTO){ header("Location:index.php?controller=".$controlador."&action=".$accion); } //Métodos para los controladores } ?>
Ahora crearemos la clase AyudaVistas que puede contener diversos helpers (pequeños métodos que nos ayuden en pequeñas tareas dentro de las vistas).
<?php class AyudaVistas{ public function url($controlador=CONTROLADOR_DEFECTO,$accion=ACCION_DEFECTO){ $urlString="index.php?controller=".$controlador."&action=".$accion; return $urlString; } //Helpers para las vistas } ?>
Ahora crearemos el fichero ControladorFrontal.func.php que tiene las funciones que se encargan de cargar un controlador u otro y una acción u otra en función de lo que se le diga por la url.
<?php //FUNCIONES PARA EL CONTROLADOR FRONTAL function cargarControlador($controller){ $controlador=ucwords($controller).'Controller'; $strFileController='controller/'.$controlador.'.php'; if(!is_file($strFileController)){ $strFileController='controller/'.ucwords(CONTROLADOR_DEFECTO).'Controller.php'; } require_once $strFileController; $controllerObj=new $controlador(); return $controllerObj; } function cargarAccion($controllerObj,$action){ $accion=$action; $controllerObj->$accion(); } function lanzarAccion($controllerObj){ if(isset($_GET["action"]) && method_exists($controllerObj, $_GET["action"])){ cargarAccion($controllerObj, $_GET["action"]); }else{ cargarAccion($controllerObj, ACCION_DEFECTO); } } ?>
El controlador frontal index.php
El controlador frontal es donde se cargan todos los ficheros de la aplicación y por tanto la única pagína que visita el usuario realmente es esta, en este caso index.php.
<?php //Configuración global require_once 'config/global.php'; //Base para los controladores require_once 'core/ControladorBase.php'; //Funciones para el controlador frontal require_once 'core/ControladorFrontal.func.php'; //Cargamos controladores y acciones if(isset($_GET["controller"])){ $controllerObj=cargarControlador($_GET["controller"]); lanzarAccion($controllerObj); }else{ $controllerObj=cargarControlador(CONTROLADOR_DEFECTO); lanzarAccion($controllerObj); } ?>
Modelos y objetos
Si queremos seguir el paradigma de la programación orientada a objetos teóricamente deberíamos tener una clase por cada tabla de la base de datos(excepto tablas pivote) que haga referencia a un objeto de la vida real, en este caso el objeto que crearíamos seria «Usuario» y el usuario tendría un nombre, un apellido, un email, etc, pues bien eso serian los atributos del objeto y tendríamos un método get y set por cada atributo que servirán para establecer el valor de las propiedades y para conseguir el valor de cada atributo. Esta clase hereda de EntidadesBase y tiene un método save para guardar el usuario en la base de datos, podríamos tener otro método update que seria similar, etc.
Y te preguntarás ¿por que no lo haces así siempre? la respuesta es simple, en algunos proyectos en los que hay muchas tablas puede ser engorroso estar creando una clase por cada tabla solamente para tener un insert y un update(aunque tiene sus ventajas) aunque según este paradigma no sea del todo correcto omito esto y directamente creo modelos de consultas en los que tengo métodos que interaccionan con una tabla mayoritariamente o varias según las relaciones que tengan, por otra parte algunos frameworks cuentan con ORMs que nos ayudan con todo esto pero de igual forma cuando tienes muchas tablas relacionadas quizá el uso del ORM sin controlarlo muy bien puede dificultar la tarea, en cualquier caso lo más correcto es tener una clase por entidad aunque a veces no sea lo más practico o cómodo.
Truco: si usas NetBeans puedes generar los getters y setters desde el menú Source -> Insert Code
<?php class Usuario extends EntidadBase{ private $id; private $nombre; private $apellido; private $email; private $password; public function __construct() { $table="usuarios"; parent::__construct($table); } public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getNombre() { return $this->nombre; } public function setNombre($nombre) { $this->nombre = $nombre; } public function getApellido() { return $this->apellido; } public function setApellido($apellido) { $this->apellido = $apellido; } public function getEmail() { return $this->email; } public function setEmail($email) { $this->email = $email; } public function getPassword() { return $this->password; } public function setPassword($password) { $this->password = $password; } public function save(){ $query="INSERT INTO usuarios (id,nombre,apellido,email,password) VALUES(NULL, '".$this->nombre."', '".$this->apellido."', '".$this->email."', '".$this->password."');"; $save=$this->db()->query($query); //$this->db()->error; return $save; } } ?>
Aquí pondríamos las consultas completas, en lugar de utilizar los métodos que tenemos en el modelo de entidad, aunque también estarían accesibles desde este modelo.
<?php class UsuariosModel extends ModeloBase{ private $table; public function __construct(){ $this->table="usuarios"; parent::__construct($this->table); } //Metodos de consulta public function getUnUsuario(){ $query="SELECT * FROM usuarios WHERE email='victor@victor.com'"; $usuario=$this->ejecutarSql($query); return $usuario; } } ?>
Los controladores
Los crearemos en el directorio controller, en este caso tengo creado UsuariosController.
<?php class UsuariosController extends ControladorBase{ public function __construct() { parent::__construct(); } public function index(){ //Creamos el objeto usuario $usuario=new Usuario(); //Conseguimos todos los usuarios $allusers=$usuario->getAll(); //Cargamos la vista index y le pasamos valores $this->view("index",array( "allusers"=>$allusers, "Hola" =>"Soy Víctor Robles" )); } public function crear(){ if(isset($_POST["nombre"])){ //Creamos un usuario $usuario=new Usuario(); $usuario->setNombre($_POST["nombre"]); $usuario->setApellido($_POST["apellido"]); $usuario->setEmail($_POST["email"]); $usuario->setPassword(sha1($_POST["password"])); $save=$usuario->save(); } $this->redirect("Usuarios", "index"); } public function borrar(){ if(isset($_GET["id"])){ $id=(int)$_GET["id"]; $usuario=new Usuario(); $usuario->deleteById($id); } $this->redirect(); } public function hola(){ $usuarios=new UsuariosModel(); $usu=$usuarios->getUnUsuario(); var_dump($usu); } } ?>
Las vistas
En este caso tengo la vista indexView.php creada.
<!DOCTYPE HTML> <html lang="es"> <head> <meta charset="utf-8"/> <title>Ejemplo PHP MySQLi POO MVC</title> <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet" type="text/css" /> <script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <style> input{ margin-top:5px; margin-bottom:5px; } .right{ float:right; } </style> </head> <body> <form action="<?php echo $helper->url("usuarios","crear"); ?>" method="post" class="col-lg-5"> <h3>Añadir usuario</h3> <hr/> Nombre: <input type="text" name="nombre" class="form-control"/> Apellido: <input type="text" name="apellido" class="form-control"/> Email: <input type="text" name="email" class="form-control"/> Contraseña: <input type="password" name="password" class="form-control"/> <input type="submit" value="enviar" class="btn btn-success"/> </form> <div class="col-lg-7"> <h3>Usuarios</h3> <hr/> </div> <section class="col-lg-7 usuario" style="height:400px;overflow-y:scroll;"> <?php foreach($allusers as $user) { //recorremos el array de objetos y obtenemos el valor de las propiedades ?> <?php echo $user->id; ?> - <?php echo $user->nombre; ?> - <?php echo $user->apellido; ?> - <?php echo $user->email; ?> <div class="right"> <a href="<?php echo $helper->url("usuarios","borrar"); ?>&id=<?php echo $user->id; ?>" class="btn btn-danger">Borrar</a> </div> <hr/> <?php } ?> </section> <footer class="col-lg-12"> <hr/> Ejemplo PHP MySQLi POO MVC - Víctor Robles - <a href="http://victorroblesweb.es">victorroblesweb.es</a> - Copyright © <?php echo date("Y"); ?> </footer> </body> </html>
El resultado «final», está cargando el controlador y acción por defecto.
Para acceder a otros controladores y acciones por la url.
Este ejemplo está hecho en unas tres horas, podría trabajarse mucho más y construirnos un marco de trabajo muy bonito para nosotros y hecho por nosotros con lo cual tendríamos un control y conocimiento absoluto de lo que pasa por debajo. El siguiente paso seria limpiar la URL mediante un .htaccess, y seguir dándole funcionalidades, añadiendo nuestras propias librerías para toda clase de tareas o de terceros como por ejemplo PHPThumb, HTML2PDF, SwiftMailer, Twig si queremos un motor de plantillas poderoso o incluso Active Record ORM para darle más potencia.
¿Por que cuento todo esto? En primer lugar para disipar las dudas del que pueda creer que no entiendo la programación orientada a objetos, en segundo lugar para demostrar que un ex alumno de un ciclo formativo de administración de sistemas sabe programar igual que cualquier otro de ciclos específicos de programación, en tercer lugar para enseñar como se hace un programa en PHP utilizando POO y MVC con un controlador frontal, y para demostrar que aunque programemos en PHP puro no tenemos porque hacer las cosas mal y no cuesta trabajo hacerlas bien, he visto varios proyectos hechos actualmente que son un verdadero caos y un despropósito, con esto podemos empezar a hacer buenos programas aunque no usemos un framework de terceros.
Correcciones y mejoras (2016)
En el ejemplo tenemos un error a la hora de llamar a varias entidades, esto es porque estamos haciendo la conexión a la base de datos varias veces, lo cual es un error. Veamos como mejorar esto:
Primero modificaremos ControladorBase para hacer que el fichero Conectar.php esté disponible en todo el código.
<?php class ControladorBase{ public function __construct() { require_once 'Conectar.php'; require_once 'EntidadBase.php'; require_once 'ModeloBase.php'; //Incluir todos los modelos foreach(glob("model/*.php") as $file){ require_once $file; } } //Plugins y funcionalidades public function view($vista,$datos){ foreach ($datos as $id_assoc => $valor) { ${$id_assoc}=$valor; } require_once 'core/AyudaVistas.php'; $helper=new AyudaVistas(); require_once 'view/'.$vista.'View.php'; } public function redirect($controlador=CONTROLADOR_DEFECTO,$accion=ACCION_DEFECTO){ header("Location:index.php?controller=".$controlador."&action=".$accion); } //Métodos para los controladores } ?>
Ahora lo que vamos a hacer es pasarle un parámetro al constructor de EntidadBase, le vamos a pasar directamente la conexión a la base de datos con el parametro $adapter:
<?php class EntidadBase{ private $table; private $db; private $conectar; public function __construct($table, $adapter) { $this->table=(string) $table; /* require_once 'Conectar.php'; $this->conectar=new Conectar(); $this->db=$this->conectar->conexion(); */ $this->conectar = null; $this->db = $adapter; } public function getConetar(){ return $this->conectar; } public function db(){ return $this->db; } public function getAll(){ $query=$this->db->query("SELECT * FROM $this->table ORDER BY id DESC"); while ($row = $query->fetch_object()) { $resultSet[]=$row; } return $resultSet; } public function getById($id){ $query=$this->db->query("SELECT * FROM $this->table WHERE id=$id"); if($row = $query->fetch_object()) { $resultSet=$row; } return $resultSet; } public function getBy($column,$value){ $query=$this->db->query("SELECT * FROM $this->table WHERE $column='$value'"); while($row = $query->fetch_object()) { $resultSet[]=$row; } return $resultSet; } public function deleteById($id){ $query=$this->db->query("DELETE FROM $this->table WHERE id=$id"); return $query; } public function deleteBy($column,$value){ $query=$this->db->query("DELETE FROM $this->table WHERE $column='$value'"); return $query; } /* * Aqui podemos montarnos un monton de métodos que nos ayuden * a hacer operaciones con la base de datos de la entidad */ } ?>
Ahora modificaremos también modelo base para pasarle la conexión:
<?php class ModeloBase extends EntidadBase{ private $table; private $fluent; public function __construct($table, $adapter) { $this->table=(string) $table; parent::__construct($table, $adapter); $this->fluent=$this->getConetar()->startFluent(); } public function fluent(){ return $this->fluent; } public function ejecutarSql($query){ $query=$this->db()->query($query); if($query==true){ if($query->num_rows>1){ while($row = $query->fetch_object()) { $resultSet[]=$row; } }elseif($query->num_rows==1){ if($row = $query->fetch_object()) { $resultSet=$row; } }else{ $resultSet=true; } }else{ $resultSet=false; } return $resultSet; } //Aqui podemos montarnos metodos para los modelos de consulta } ?>
Ahora nuestras entidades recibirán la conexión como parametro, de esta forma:
<?php class Usuario extends EntidadBase{ private $id; private $nombre; private $apellido; private $email; private $password; public function __construct($adapter) { $table="usuarios"; parent::__construct($table, $adapter); } public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getNombre() { return $this->nombre; } public function setNombre($nombre) { $this->nombre = $nombre; } public function getApellido() { return $this->apellido; } public function setApellido($apellido) { $this->apellido = $apellido; } public function getEmail() { return $this->email; } public function setEmail($email) { $this->email = $email; } public function getPassword() { return $this->password; } public function setPassword($password) { $this->password = $password; } public function save(){ $query="INSERT INTO usuarios (id,nombre,apellido,email,password) VALUES(NULL, '".$this->nombre."', '".$this->apellido."', '".$this->email."', '".$this->password."');"; $save=$this->db()->query($query); //$this->db()->error; return $save; } } ?>
He creado una nueva entidad Producto para ver como se usa:
<?php class Producto extends EntidadBase{ private $id; private $nombre; private $precio; private $marca; public function __construct($adapter) { $table="productos"; parent::__construct($table,$adapter); } public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getNombre() { return $this->nombre; } public function setNombre($nombre) { $this->nombre = $nombre; } public function getPrecio() { return $this->precio; } public function setPrecio($precio) { $this->precio = $precio; } public function getMarca() { return $this->marca; } public function setMarca($marca) { $this->marca = $marca; } public function save(){ $query="INSERT INTO productos (id,nombre,precio,marca) VALUES(NULL, '".$this->nombre."', '".$this->precio."', '".$this->marca."');"; $save=$this->db()->query($query); //$this->db()->error; return $save; } } ?>
Además también tenemos que modificar nuestros modelos de consulta para que reciban el «adaptador» de la conexión:
<?php class UsuariosModel extends ModeloBase{ private $table; public function __construct($adapter){ $this->table="usuarios"; parent::__construct($this->table, $adapter); } //Metodos de consulta public function getUnUsuario(){ $query="SELECT * FROM usuarios WHERE email='victor@victor.com'"; $usuario=$this->ejecutarSql($query); return $usuario; } } ?>
Ahora en el constuctor de cada uno de nuestros controladores tendremos que instanciar la conexión y utilizar la propiedad adapter para pasar la conexión a los modelos y entidades:
<?php class UsuariosController extends ControladorBase{ public $conectar; public $adapter; public function __construct() { parent::__construct(); $this->conectar=new Conectar(); $this->adapter=$this->conectar->conexion(); } public function index(){ //Creamos el objeto usuario $usuario=new Usuario($this->adapter); //Conseguimos todos los usuarios $allusers=$usuario->getAll(); //Producto $producto=new Producto($this->adapter); $allproducts=$producto->getAll(); //Cargamos la vista index y le pasamos valores $this->view("index",array( "allusers"=>$allusers, "allproducts" => $allproducts, "Hola" =>"Soy Víctor Robles" )); } public function crear(){ if(isset($_POST["nombre"])){ //Creamos un usuario $usuario=new Usuario($this->adapter); $usuario->setNombre($_POST["nombre"]); $usuario->setApellido($_POST["apellido"]); $usuario->setEmail($_POST["email"]); $usuario->setPassword(sha1($_POST["password"])); $save=$usuario->save(); } $this->redirect("Usuarios", "index"); } public function borrar(){ if(isset($_GET["id"])){ $id=(int)$_GET["id"]; $usuario=new Usuario($this->adapter); $usuario->deleteById($id); } $this->redirect(); } public function hola(){ $usuarios=new UsuariosModel($this->adapter); $usu=$usuarios->getUnUsuario(); var_dump($usu); } } ?>
A la vista le podemos añadir lo siguiente para que muestre el listado de productos:
<?php if(isset($allproducts) && count($allproducts)>=1) {?> <div class="col-lg-7"> <h3>Productos</h3> <hr/> </div> <section class="col-lg-7 producto" style="height:400px;overflow-y:scroll;"> <?php foreach($allproducts as $product) {?> <?php echo $product->id; ?> - <?php echo $product->nombre; ?> - <?php echo $product->precio; ?> - <?php echo $product->marca; ?> <hr/> <?php } ?> </section> <?php } ?>
Y con esto ya tenemos el mini framework funcionando para empezar a trabajar mejor con el. Este ejemplo está muy bien, pero para crear tus proyectos reales te recomiendo que uses un framework estandar con una comunidad de detrás y que te permita hacer aplicaciones web más robustas como por ejemplo Symfony o Laravel.
Código fuente de este ejemplo
Base de datos del ejemplo
Vídeo tutorial paso por paso:
Puedes aprender mas sobre PHP, el MVC y los frameworks en mi curso gratuito de introducción a los framworks para PHP.