Desarrollar un proyecto MVC con PHP puro.
Tags: webdev, php, tutorial
Published: 2022-11-09 01:56:17

### Si quieres desarrollar un proyecto web en PHP te presento una manera de como estructurar tu proyecto con buenas practicas y con el patron de diseño MVC.

Empezemos primero con la estructura de carpetas en tu editor de codigo o IDE favorito:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y5o2h3f8lzjteak4leuo.PNG)

Se compone de dos carpetas principales: app/ & public/

**En la carpeta public/:**

Tendremos carpetas donde tendremos archivos estaticos, carpetas como:

- css/
- js/
- img/

entre otras carpetas como por ejemplo puede ser:
- fonts/

Adicional un archivo llamado **index.php** en el cual se va cargar lo necesario de la carpeta **app/**, para mostrar las vistas del proyecto en conjunto con los modelos y controladores, que al fin y al cabo la estructura del proyecto es MVC (modelo, vista, controlador).

**En la carpeta app/:**

Tendremos la logica de negocio, osea, tendremos el control de las vistas y los modelos en un controlador. 
Carpetas como: 

- config/
- lib/
- helpers/
- models/
- views/
- controllers/

Y adicional un archivo llamado **init.php**.

A medida que vayamos empezando iremos agregando archivos en las carpetas para explicar en que consiste cada archivo.

Si pudiste observar en la imagen de arriba, hay archivos llamados **.htaccess**. Estos archivos nos ayudaran mucho para evitar acceso a carpetas y mapear URL, veremos poco a poco en que consisten, pero antes te dire en donde iran ubicados estos archivos:

El primer **.htaccess** lo crearemos en la carpeta raiz del proyecto: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtsb9ejsv2p8a4acf54a.PNG)

El segundo **.htaccess** lo crearemos en la carpeta **app/**:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i72xnevy2cx0fy0hdhuc.PNG)

Y el tercer **.htaccess** lo crearemos en la carpeta **public/**:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n4grxnrq0bot25nn8zmd.PNG)

### A continuación escribiremos las instrucciones en los archivos **.htaccess**:

- Empezamos por el **.htaccess** de la carpeta **app/** con la siguiente instruccion:

```
Options -Indexes
```

Con esa instruccion haremos que no se tenga acceso a esa carpeta ya que tendremos la logica del negocio, el funcionamiento de la aplicacion web y no nos interesa que tengan acceso a dichas carpetas de personas (mal intencionadas) en caso de que asi sea.

- Continuaremos por el **.htaccess** de la carpeta **public/**:

Antes de explicar estas instrucciones debemos saber que por defecto el primer archivo que lee el navegador es el **index**. Ahora bien, sucede lo siguiente: Al nosotros digitar una url con un archivo que no existe por ejemplo **localhost/mvc-php/about.php** pues el servidor nos lanza un error **500** llamado **Internal Server Error** ya que el archivo **about.php** no lo tenemos en nuestro proyecto.

Es por eso que escribimos las siguientes instrucciones:

```
<IfModule mod_rewrite.c>
Options -Multiviews
RewriteEngine on
RewriteBase /mvc-php/public
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]
</IfModule>
```

Con estas instrucciones hacemos que se eviten las **Multiviews** es decir que en nuestro proyecto **/mvc-php/public** si no encuentra un archivo en la carpeta **public/** que no existe pues, redireccionar automaticamente al **index.php**, adicionalmente pasamos un argumento llamado **?url** en donde mas adelante veremos como podremos mapear la URL a travez de ese argumento y extraer esos datos con la funcion **explode()** de php.

Recuerda que nuestra carpeta o proyecto se llama **mvc-php** por eso hemos escrito ese nombre en nuestro archivo **.htaccess**. Si tu proyecto o carpeta se llamara diferente debes ponerle el nombre de tu proyecto para evitar problemas a la hora de correr la web.

- Terminaremos por el **.htaccess** de la carpeta raiz:

Antes de explicar las instrucciones, sucede una cosa, cuando abrimos nuestro proyecto en la web a travez de un servidor local nos muestra el directorio de nuestro proyecto:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/br95hrn64ahvegwa8kvy.PNG)

Para evitar esto, escribiremos las siguientes instrucciones:

```
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]
</IfModule>
```

Con estas instrucciones hacemos que nos lea de principio a fin **(^$)** seguido de la carpeta **public/** y nos cargue un archivo con cualquier extension **(.*)**. Este archivo se lo pasamos a travez de la URL en el argumento **?url=$1** del archivo **.htaccess** de la carpeta **public/**

Hasta acá hemos terminado con las instrucciones de los archivos **.htaccess**

### A continuación empezaremos a trabajar con la URL para el funcionamiento de nuestro proyecto:

- Dentro de la carpeta **app/** iremos a la carpeta **lib/** y crearemos el archivo **Core.php** y escribiremos el siguiente codigo: 

```
class Core
{

  public function __construct()
  {
    $this->getUrl();
  }

  public function getUrl()
  {
    echo $_GET['url'];
  }
}
```

Estamos obteniendo la **url** del navegador, ya explicare de donde sacamos la **url**, continuando, iremos al fichero **index.php** de la carpeta **public/** y escribiremos lo siguiente:

```
require_once '../app/init.php';
```

Recordemos que en el archivo **index.php** de la carpeta **public/** cargaremos todo lo necesario de la carpeta **app/**

Continuando, iremos al archivo **init.php** de la carpeta **app/** y por el momento escribiremos lo siguiente:

```
echo "<h1>HELLO WORLD!!!</h1>";
```

Abramos nuestro proyecto en el navegador por medio del servidor local y veremos el mensaje de **HELLO WORLD**, si es así, vamos por buen camino.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c5lz2qf0wlctf3doau69.PNG)

Esto era prueba para verificar que funcione lo que tratamos de hacer, volvemos al archivo **init.php** de la carpeta **app/** y cargaremos los ficheros de la carpeta **lib/** con las siguientes lineas de codigo: 

```
spl_autoload_register(function($lib){
  require_once 'lib/' . $lib . '.php';
});
```

Con esto hacemos que vayamos cargando automaticamente los ficheros de la carpeta **lib/**, luego, para mapear la URL lo haremos en el fichero de **Core.php**, entonces debemos instanciar la clase que tenemos en **Core.php** en el archivo **index.php** de la carpeta **public/** con la siguiente linea de codigo:

```
$init = new Core;
```

Ahora recargaremos en el navegador nuestro proyecto y no nos debe aparecer nada. Si es asi vamos por buen camino:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/stzm1jn6u6vw1dbn2cur.PNG)

Ahora para probar que estamos obteniendo la URL digitaremos algo en nuestra URL por ejemplo: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wl0eketlxg5zlww7l23.PNG)

Podemos ver que se imprime la URL, explicaré a detalle como es que obtenemos lo que escribimos despues de **localhost/mvc-php/**. Recordemos que escribimos algunas instrucciones en el archivo **.htaccess** de la carpeta **public/** y en pocas palabras evitabamos las Multiviews, es decir, si buscabamos un archivo que no existe en nuestro proyecto pues que nos redireccione al **index.php** y a la vez indicabamos que la base de nuestro proyecto seria **/mvc-php/public** ya que en esa direccion se encuentra pues valga la redundancia el **index.php**, luego, pasabamos un parametro **?url=$1** donde todo lo que escribamos despues de nuestra base lo obtendriamos a travez de ese parametro. Por eso es que cuando escribimos una direccion localhost/mvc-php/**usuarios/listado.php** pues nos imprime justamente **usuarios/listado.php**

- Continuando, seguiremos de por lleno en el fichero **Core.php**:

Recordemos que teniamos una clase con un constructor y un método donde obteniamos la URL. En el método **getUrl()** borraremos lo que teniamos y escribiremos las siguientes lineas de codigo:

```
public function getUrl()
{
  if(isset($_GET['url']))
  {
    $url = rtrim($_GET['url'], '/');
    $url = filter_var($url, FILTER_SANITIZE_URL);
    $url = explode('/', $url);

    return $url;
  }
}
```

Evaluamos si existe un valor en **url**, si es así entonces, obtenemos la **url** y retiramos espacios en blanco al final de la cadena con la funcion **rtrim()**, fijate que agregamos un parametro adicional (/), esto es para que nos elimine ese caracter también ya que una URL se separa por ese caracter.

Continuando, filtramos la **url** con la funcion **filter_var()** y lo filtramos como **FILTER_SANITIZE_URL**. Esto es para que lo considere como una URL en sí.

Luego con la funcion **explode()** guardamos en un array los valores de la URL separados por el caracter **/**, es decir, va a recortar una cadena de texto cuando encuentre el caracter **/**. Y con esto estariamos mapeando la URL.

Para probar que funciona, en el constructor de la clase escribiremos lo siguiente:

```
public function __construct()
{
  print_r($this->getUrl());
}
```

Ahora veamos en el navegador y escribamos algo en la URL para que nos devuelva los valores en un array:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1na4zdand6r20u9tj79d.PNG)

Observemos que en el **indice [0]** se guardo **usuarios**, en el **indice[1]** se guardo **mostrar-usuarios**, y en el **indice[2]** se guardo pues el numero **2**. Ahora sí, ya tenemos mapeada la URL.

Vamos a terminar de escribir las ultimas lineas de codigo en el constructor, pero antes, debemos definir unas variables, y son las siguientes:

```
protected $controller;
protected $method;
protected $parameters = [];
```

Tengamos en cuenta que la **url** nos devuelve un array lo cual lo interpretaremos de la siguiente manera:

`indice[0] va ser igual al controlador`
`indice[1] va ser igual al metodo del controlador`
`indice[2] va ser igual a los parametros que iran como argumento en los metodos`

Luego dentro del constructor escribiremos las siguientes lineas de codigo:

```
public function __construct()
{
  $url = $this->getUrl();

  if(file_exists('../app/controllers/' . ucwords($url[0]) . '.php'))
  {
    $this->controller = ucwords($url[0]);
    unset($url[0]);
  }

  require_once '../app/controllers/' . $this->controller . '.php';
  $this->controller = new $this->controller;

  if(isset($url[1]))
  {
    if(method_exists($this->controller, $url[1]))
    {
      $this->method = $url[1];
      unset($url[1]);
    }
  }

  $this->parameters = $url ? array_values($url) : [];
  call_user_func_array([$this->controller, $this->method], $this->parameters);
}
```

Empezemos explicando, primero evaluamos si existe un controlador en la carpeta **controllers/** con la funcion **file_exists** y tambien utilizamos la funcion **ucwords()** para convertir en mayusculas la primer letra ya que por ende estaremos usando clases y las clases empiezan con una letra mayuscula. Entonces, si el archivo existe procedemos a asignar el valor del **indice[0] de la url** a la variable **$controller**, asi mismo utilizamos la funcion **unset()** para destruir el valor anterior del **indice[0]**. Luego lo que hacemos es requerir ese archivo y como es una clase hacemos instancia de la misma.

Segundo, evaluamos si existe un valor en el **indice[1]** osea, un metodo, y este metodo se encuentra dentro del controlador entonces tambien evaluamos si el metodo existe con la funcion **method_exists**, si el metodo existe pues asignamos el valor del **indice[1] de la url** a la variable **$method**, y de igual forma utilizamos la funcion **unset()** para destruir el valor anterios del **indice[1]**.

Tercero, con los parametros recordemos que le asignamos un array vacio, entonces vamos asignar a **$parameters** el valor de **$url** y pues **$url** es otro array con valores. Si te fijas, usamos operadores ternarios, es decir si **$parameters** es igual a **url** entonces utilizamos la funcion **array_values()** que lo que hace es devolver los valores de un array, con eso evitamos guardar un array dentro de otro array. Y si no hay nada en **$parameters** pues seguira siendo un array vacio. Para finalizar utilizamos la funcion **call_user_func_array()** y esta recibe dos parametros, primero, recibe un callback, segundo, el parametro a pasar a la callback es por eso que pasamos como callback un array con los valores de **$controller** y **$method**, y pues el parametro que se va a pasar es el de **$parameters**.

Hasta acá hemos terminado de configurar el **Core.php**, ahora bien, como saber si esto funciona. Pues vamos a la carpeta **controllers/** y crearemos un archivo, yo lo llamare **Views.php**, recordemos que estas deben ser clases. Dentro escribiremos algunos metodos, esto lo haremos con fin de demostrar, para seguir el ejemplo puedes escribir las siguientes lineas de codigo:

```
class Views
{

  public function inicio()
  {
    echo "<h1>Pagina de Inicio</h1>";
  }

  public function update($id)
  {
    echo "Update view " . $id;
  }

}
```

Vayamos al navegador y escribamos la direcion indicando el controlador y el metodo que queremos que se cargue: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lmjjx4si53k9zko1z37g.PNG)

Y en efecto vemos que se imprime lo que hemos indicado en el metodo **inicio()**.

Probemos con el otro metodo:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kdw8adg2pp8kzg2jruaz.PNG)

Y en efecto vemos que recibe el parametro e imprime lo que hemos indicado en el metodo **update($parametro)**.

### A continuacion agregaremos otro fichero a la carpeta **lib/** llamado **Control.php**

Cual será la función de este archivo, pues, se encargará de cargar los modelos y las vistas de sus respectivas carpetas, osea, desde **models/** y de **views/pages/**

Entonces escribiremos las siguientes lineas de codigo:

```
class Control
{
  public function load_model($model)
  {
    require_once '../app/models/' . $model . '.php';

    return new $model;
  }

  public function load_view($view, $datos = [])
  {
    if(file_exists('../app/views/pages/' . $view . '.php'))
    {
      require_once '../app/views/pages/' . $view . '.php';
    }
    else
    {
      die("404 NOT FOUND");
    }
  }
}
```

Explicando, pues el metodo **load_model($model)** se encarga de requerir el modelo que vamos a utilizar en el controlador. Y el metodo **load_view($view, $datos = [])** se encarga de requerir la vista que queremos que se cargue en el controlador, adicional podemos mandar un array de datos para mostrarlo en la vista.

Ya una vez teniendo esta clase con los metodos que nos ayudaran a cargar los modelos y las vistas, haremos que la clase **View.php** herede de **Control.php** para tener acceso a esos metodos, y eso se hace de la siguiente manera:

`class Views extends Control`

Ahora, antes de probar esos metodos debemos de terminar de configurar nuestro proyecto.

- En la carpeta **config/** crearemos un fichero llamado **config.php**, y escribiremos lo siguiente:

```
define('APP', dirname(dirname(__FILE__)));
define('URL', 'http://localhost/mvc-php');
```

En pocas palabras estamos definiendo dos constantes y como valor estamos asignando una ruta, en el caso de la constante **APP** la ruta es hacia las carpetas dentro de **APP**, y en el caso de **URL** estamos definiendo la url del proyecto, esto nos ayudara a tener acceso de archivos de ambas carpetas.

- Para que este archivo nos funcione, lo vamos a requerir en **init.php** y vamos a escribir lo siguiente:

`require_once 'config/config.php';`

- Luego nos iremos a la carpeta de **views/** y a la carpeta de **inc/** donde crearemos dos archivos llamados **header.php** y **footer.php**.

Primero editaremos el fichero de **header.php** y escribiremos lo siguiente:

```
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="<?= URL . '/css/bootstrap.min.css' ?>">
  <title><?= $datos['title'] ?></title>
</head>
<body>
```

Simplemente es codigo HTML5 con la diferencia que estamos cargando estilos CSS3, en este caso pues de Bootstrap. Ademas, fijate que en **title** tenemos codigo PHP, esto es porque cuando vayamos a cargar una vista desde el controlador le podemos pasar parametros en un array, y cuando le demos un valor a title se imprimira automaticamente ahí.

Ahora en **footer.php** escribiremos lo siguiente:

```
  <script src="<?= URL . '/js/bootstrap.bundle.min.js' ?>"></script>
</body>
</html>
```

Y pues, es codigo HTML5, estamos cargando el archivo JavaScript de Bootstrap, y a la vez estamos cerrando las etiquetas de **body** y **html**.

Estos archivos son repetitivos, por lo tanto con esto nos ahorramos escribir en cada vista este codigo.

- Ahora vayamos a la carpeta de **views/** y a la carpeta de **pages/** y crearemos un fichero llamado **inicio.php** y escribiremos lo siguiente:

```
<?php require_once APP . '/views/inc/header.php' ?>

<div class="container-fluid bg-light py-5">
  <div class="container">
    <h1 class="display-4">Welcome!!!</h1>
  </div>
</div>

<?php require_once APP . '/views/inc/footer.php' ?>
```

Fijate que estamos cargando **header.php** al inicio, y tambien estamos cargando **footer.php** al final. Con esto, cada vez que vayamos a escribir codigo HTML5 ya tendremos el codigo reutlizable y solo lo tendremos que requerir con **require()**. Esto nos ayudara a optimizar un poco el codigo en estos archivos.

También estamos mostrando pues un mensaje **Welcome** con algunas clases de bootstrap para darle estilos a nuestras paginas.

- Ya teniendo esto, hoy si vayamos a **Views.php** dentro de **controllers/** y editaremos el metodo llamado **inicio()**:

```
public function inicio()
{
  $datos = [
    "title" => "Inicio"
  ];
  $this->load_view('inicio', $datos);
}
```

Fijate que en el metodo **inicio()**, primero, asignamos un array llamado **$datos** y creamos un **key** llamado **title**, el valor que este obtenga se mostrara en la etiqueta **<title></title>** de cada pagina. Luego hacemos uso del metodo que nos ayuda a cargar las **views/pages** que queremos mostrar, solamente indicamos el nombre de la vista, y le pasamos el array de **$datos** para que tambien muestre el titulo de la pagina.

### Finalmente, probemos como es que va terminar funcionando:

- Vayamos al navegador y nos dirigimos a la URL de nuestro proyecto:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d853x8k2yrru2jm5bsxv.PNG)

No nos aparecera nada, pero si en la URL indicamos el controlador y el metodo que queremos ver, nos muestra lo siguiente:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/53hypgh31loayvmort2w.PNG)

Y vemos que se carga la pagina de inicio que habiamos creado en **pages/views/inicio.php** por medio del controlador y el metodo dentro del cual requerimos esa vista con el metodo **load_view()**.

### CONCLUSIONES:

Hasta aquí hemos terminado de cierta manera el funcionamiento del proyecto, el cual consiste en aplicar el patron de diseño MVC (modelo, vista, controlador) y a la vez aplicar POO (programacion orientada a objetos). También decir que, consideraría este proyecto como un **template** donde puedes usarlo para crear un proyecto para desarrollar un **CRUD**.

**Hasta acá dejaré desarrollado este proyecto, y posteriormente subiré otra nota donde usaremos este mismo proyecto pero realizando un CRUD y como poder desarrollarlo.**

#### Te dejo el link del repositorio en GITHUB y puedas usar este template para desarrollar un proyecto.

Si puedes deja una estrella al repositorio :p

[template-mvc-php](https://github.com/ulisesafcdev/mvc-php)