Lenguaje PHP
[TOC]
Introducción
PHP
¿Qué es PHP?
PHP es un lenguaje de programación de código abierto del lado del servidor. En un inicio PHP representaba Personal Home Page, hoy en día es el acrónimo recursivo PHP: Hypertext Preprocessor. PHP puede combinar código de PHP y HTML en un mismo archivo.
Logotipo
El logotipo de PHP es el siguiente
Y con la llegada de PHP7 el logotipo pasó a tomar un estilo mas actual.
ElePHPant
PHP tiene un elefante como mascota de nombre ElePHPant que fue creado por allá del año de 1998 por un francés de nombre Vincent Pontier, él mismo es quien diseñó el logotipo de PHP7 y es curioso como el elefante salio de visualizar las letras PHP desde cierto ángulo.
Referencia del lenguaje
Archivos PHP
Generalmente PHP utiliza la extensión .php para representar a sus archivos pero ciertas veces suelen utilizarse las extensiones .inc para archivos que serán incluidos y .class.php para archivos de clase. Eso es por conveniencia de quien los utiliza pero no es regla, aunque lo más común seria utilizar la extensión .php.
Etiquetas de PHP
Cuando PHP analiza un archivo, busca las etiquetas de apertura y cierre que le dicen a PHP donde comenzar y terminar de interpretar el código PHP entre ellas. Todo lo demás fuera de las etiquetas es ignorado por el interprete de PHP.
Las etiquetas de apertura y cierre de PHP son <?php
y ?>
respectivamente. Además PHP permite el uso de las short open tags <?
las cuales se desaconseja utilizar ya que solo están disponibles si se habilita la opción short open tag desde el archivo de configuración php.ini.
¡Por convención nunca utilices las Short Open Tags!
Escapando código desde HTML
Todo fuera de las etiquetas de apertura y cierre de PHP es ignorado por el interprete de PHP el cual permite tener contenido mixto. Esto permite a PHP ser embedido en documentos HTML, por ejemplo, para crear una plantilla.
<p>Esto será ignorado por el interprete de PHP y será desplegado en el navegador.</p>
<?php echo 'Mientras que esto será interpretado por PHP.'; ?>
<p>Esto también será ignorado por PHP y desplegado por el navegador.</p>
Short Echo Tags
Si necesitamos mandar como salida algún texto almacenado en una variable podemos realizarlo de la siguiente manera:
<?php echo $variable; ?>
Pero a partir de la versión 5.4 de PHP se pueden utilizar las Short Echo Tags para mandar como salida valores de variables de una manera un poco mas cómoda.
<?= $variable =>
Tome en cuenta que dentro de las short echo tags no es posible realizar ninguna operación de cualquier tipo que no sea utilizar una variable para mandar su contenido como salida.
Utilizando estructuras con condiciones
En el siguiente ejemplo PHP se saltará los bloques donde la condición no se cumpla, inclusive si los bloques se encuentran fuera de las etiquetas de apertura y cierre de PHP.
<?php if ($expression == true): ?>
Esto se mostrará si la expresión es verdadera.
<?php else: ?>
De otro modo esto será mostrado.
<?php endif; ?>
Para desplegar como salida grandes cantidades de texto es mas eficiente realizarlo fuera de cualquier bloque de análisis de código del interprete de PHP que hacerlo mediante una instrucción echo
o print
.
Separador de instrucciones
Así como en C o Perl, PHP requiere que las instrucciones sean terminadas con un punto y coma al final de cada sentencia. La etiqueta de cierre de un bloque de código de PHP automáticamente implica un punto y coma; no necesitas tener un punto y coma para terminar la última línea de un bloque código de PHP cuando exista una etiqueta de cierre. La etiqueta de cierre de un bloque puede omitirse cuando se halla alcanzado el final del archivo y será necesario colocar un punto y coma para terminar la expresión.
<?php
echo 'Esta es una prueba';
?>
<?php echo 'Sin punto y coma por que existe una etiqueta de cierre.' ?>
<?php echo 'Sin la etiqueta de cierre, pero con un punto y coma';
Pro Tip: Si el archivo con el que estas trabajando solo contiene código PHP omite la etiqueta de cierre, te ahorraras ciertos problemas al utilizar el archivo con instrucciones require o include cuando existan espacios extras que hallas olvidado omitir.
Comentarios
PHP soporta comentarios al estilo C, C++ y Unix shell (Perl style). Por ejemplo:
<?php
echo 'Esta es una prueba'; // Este es un comentario de una linea al estilo C++.
/* Este es un comentario
multi-linea */
echo 'Esta es otra prueba';
echo 'Una última prueba'; # Este es un comentario de una linea al estilo shell
Namespaces
Un namespace es una manera de encapsular elementos. Esto puede verse como un concepto abstracto en muchos lugares. Por ejemplo, cualquier directorio de un sistema operativo funciona como un grupo de archivos relacionados, y actua como un namespace para los archivos. Como un ejemplo concreto, el archivo foo.txt
puede existir en ambos directorios /home/greg
y /home/other
, pero dos copias de foo.txt
no deben co-existir en el mismo directorio. Adicionalmente, para acceder al archivo foo.txt
por fuera del directorio /home/greg
, deberemos incluir el nombre del directorio al nombre del archivo utilizando el separador de directorios para obtener /home/greg/foo.txt
. Este mismo principio se extiende para los namespaces en el mundo de la programación.
En el mundo de PHP, los namespaces son diseñados para resolver dos problemas que los autores de librerías y aplicaciones se encuentran cuando crean elementos de código re-utilizables como clases y funciones:
- La colisión de nombres entre código que creas y clases/funciones/constantes internas de PHP o clases/funciones/constantes de proveedores externos.
- La habilidad para renombrar (o acortar) Nombres_Extra_Largos diseñados para solucionar el problema del punto anterior, promoviendo lectura del código fuente.
Los namespaces de PHP proveen una manera en la cual podemos agrupar clases, interfaces, funciones y constantes relacionadas.
Para efectos de rapidez definiremos los namespaces de acuerdo al estándar de programación PSR-2.
Un namespace se declara utilizando la palabra reservada namespace
seguida por el nombre que queremos proporcionar namespace. Por ejemplo:
<?php
namespace Coppel;
Un namespace puede contener varios niveles de agrupación los cuales podemos definir utilizando el separador \
(barra invertida).
<?php
namespace Coppel\Database\PostgreSql;
- Un namespace debe declararse al inicio del archivo o después de la definición del bloque de inicio de PHP (<?php).
- El namespaces deberá iniciar con el nombre del proveedor o vendor.
Ver namespaces en php.net.
PDO
PHP Data Objects es una extensión que define una interfaz ligera y consistente para acceder a bases de datos en PHP. Cada driver de base de datos que implementa la interfaz PDO puede exponer caracteristias especificas de base de datos así como las funciones regulares de la extensión. Hay que tomar en cuenta que no se puede realizar cualquier funcionalidad de base de datos usando la extensión PDO por si sola; debe de utilizar un driver especifico para acceder a el servidor de base de datos que necesite conectarse.
PDO provee una capa de abstracción de acceso a datos, lo que quiere decir que, sin importar que base de datos estés utilizando, usas las mismas funciones para realizar consultas y obtener datos.
PDO está disponible desde PHP 5.1 y requiere las características de programación orientada a objetos de PHP 5, por lo tanto no funciona con versiones anteriores.
Sesiones
PHP tiene soporte para el manejo y administración de sesiones la cual es una manera de preservar cierta información a través de accesos subsecuentes. Esto permite construir aplicaciones mas personalizadas e incrementar la experiencia de navegación.
Build-in server
Sobre el proyecto PHP
Vamos a realizar un proyecto en el cual se podrán realizar compras de articulos de manera electrónica el cual deberá cubrir los siguientes requerimientos:
- Registarse como usuario.
- Iniciar sesión.
- Buscar productos.
- Páginar el listado de los productos.
- Filtrar productos por categorías.
- Ver el detalle del producto.
- Agregar productos al carrito de compras.
- Hacer el checkout de los productos.
Sobre el framework de desarrollo
El proyecto lo podemos realizar de una manera sencilla y ordenada en un solo proyecto, pero para cuestiones de conocer un poco mas como funciona todo vamos a implementar y desarrollar un micro-framework el cual deberá de cubrir los siguientes requerimientos:
- Implementar el patrón MVC.
- Implementar un mecanismo de ruteo de urls.
- Contar con un template engine el cual contenga un contexto de variables.
- Implementar una configuración general.
- Crear conexiones a base de datos.
- Configurar la estructura de directorios de los controladores, modelos, vistas y assets.
Tanto como el proyecto y el framework deberán utilizar el gestor de paquetes Composer para realizar la auto carga de clases.
Estructura del proyecto PHP.
El proyecto (directorio) deberá llamarse shoppingcart
y dentro de el tendremos la siguiente estructura de directorios:
shoppingcart/
|-- composer.json
|-- index.php
|-- assets
| |-- css
| |-- fonts
| |-- images
| |-- js
|-- src
| |-- config
| |-- controllers
| |-- models
| |-- views
|-- vendor
Cada archivo/directorio tiene un fin en especifico:
- composer.json: archivo de configuración de composer, se auto-genera cuando ejecutamos la instrucción
composer init
. - index.php: archivo de entrada del proyecto, este archivo es el que se ejecuta inicialmente cuando una petición HTTP entra al servidor.
- assets: archivos son archivos que representan recursos tales como imágenes, archivos de scripts, hojas de estilo, fuentes y todo otro recurso que el sitio web pudiera necesitar.
- src: directorio que contiene el código fuente del proyecto.
- config: archivos de configuración de la aplicación.
- controllers: clases PHP de tipo controlador.
- models: clases PHP de tipo modelo.
- views: vistas de la aplicación.
- vendor: directorio creada por composer la cual contiene archivos de configuración de las dependencias y de la auto carga de clases.
Ejecutando composer
Iniciamos creando los archivos de composer para manejar las dependencias (no en este proyecto, pero se pudiera hacer) y la auto carga de clases (esto si lo haremos).
En la terminal deberemos situarnos dentro del directorio shoppingcart
y ejecutar el siguiente comando:
composer init
Aparecerá un mensaje como este Welcome to the Composer config generator
seguido por otro mensaje que solicita ingresar el nombre del paquete (proyecto) que estamos creando. Como se indica en el mensaje es necesario ingresar un vendor y un name, vendor es como el propietario del proyecto y nombre el nombre que llevará el proyecto. Ingresemos lo siguiente: coppel/shoppingcart
.
Note que el
package name
debe ser en minúsculas, trate de ingresar el nombre del proyecto o el propietario en mayúsculas y aparecerá un mensaje de error indicando que el nombre del paquete es incorrecto.
Después composer solicitará una descripción del proyecto, aquí ingrese lo que crea que sea necesario. Al solicitar el autor es necesario ingresar un valor con el siguiente formato: name my@email, por ejemplo Juan Perez <jperez@coppel.com>
.
Cuando solicite Minimum Stability
, Package Type
y Licence
puede solamente presionar la tecla <Enter>
para dejar esas propiedades vacias.
Lo siguiente son dos preguntas que son referentes a las dependencias y como en este proyecto no manejaremos dependencias deberemos de ingresar los valores no
en las preguntas Would you like to define your dependencies (require) interactively [yes]?
y Would you like to define your dev dependencies (require-dev) interactively [yes]?
y presionar la tecla <Enter>
.
Para terminar deberemos de confirmar todos los datos que hemos ingresado ingresando el valor yes
y presionar la tecla <Enter>
.
Realizado lo anterior podemos observar que dentro del proyecto existe un archivo de nombre composer.json
el cual contiene toda la información que ingresamos con el uso del comando composer init
.
{
"name": "coppel/shoppingcart",
"description": "Tienda en linea para la venta de artículos",
"authors": [
{
"name": "Juan Perez",
"email": "jperez@coppel.com"
}
],
"require": {}
}
Ahora vamos a ejecutar el siguiente comando en la terminal.
composer dump-autoload
Con esto le decimos a composer que genere los archivos de configuración de auto carga los cuales se crean en el directorio vendor
. El directorio vendor
contiene lo siguiente:
vendor/
|-- autoload.php
|-- composer
|-- autoload_classmap.php
|-- autoload_namespaces.php
|-- autoload_psr4.php
|-- autoload_real.php
|-- ClassLoader.php
En este momento es irrelevante lo que contiene cada uno de los archivos pero mas adelante veremos para que se necesitan algunos de los archivos cuando configuremos la auto carga de clases.
Estructura del framework de desarrollo
El framework que crearemos será sumamente sencillo, conteniendo tareas básicas como las mencionadas en anteriormente. Vamos a elegir un nombre para el framework, puedes elegir el que gustes, yo utilizaré el nombre Yelu
el cual lo he sacado de la palabra Yellow
en ingles haciendo referencia al color primario que utiliza la empresa para su marca comercial.
Entonces vamos a crear el directorio Yelu
en el mismo directorio en el cual creamos el proyecto shoppingcart
y este contendrá la siguiente estructura.
yelu/
|-- composer.json
|-- src
| |-- BaseController.php
| |-- BaseModel.php
| |-- Router.php
| |-- Yelu.php
|-- vendor
Como ven, en la estructura del proyecto existe el archivo composer.json
y el directorio vendor
por lo cual deberemos ejecutar nuevamente el comando composer init
dentro del directorio del framework. Para este caso, podemos ingresar los valores de la misma manera en la que los ingresamos para el proyecto shoppingcart
, solo que para el framework vamos a utilizar otro package name
, usaremos coppel/yelu
.
Si utilizaste un nombre diferente para el framework solo deberás reemplazarlo en la parte del
package name
, por ejemplocoppel/myawesomename
.
Escribiendo el framework de desarrollo.
En el proyecto Yelu
vamos a crear el directorio src
y dentro de el vamos a crear el archivo Yelu.php
.
Nuevamente, si usted creo el framework con otro nombre lo mas apropiado será crear un archivo de nombre
MyAwesomeFamework.php
.
Dentro de este archivo vamos a crear la clase Yelu
con el namespace Coppel\Yelu
.
<?php
namespace Coppel\Yelu;
class Yelu
{
public function __construct()
{
echo __METHOD__;
}
}
Lo siguiente es cargar la clase desde el proyecto shoppingcart
, por lo cual vamos a crear el archivo index.php
que se encuentra en la raíz del proyecto y dentro de el vamos a ingresar lo siguiente.
<?php
require_once "./vendor/autoload.php";
$yelu = new Coppel\Yelu\Yelu();
Ahora, si ejecutamos lo siguiente en la terminal:
php index.php
Deberá de aparecer un mensaje como el siguiente:
PHP Fatal error: Class 'Coppel\Yelu\Yelu' not found in /shoppingcart/index.php on line 5
La clase no ha sido encontrada y esto es por que no hemos configurado composer para que auto cargue las clases. Para ello vamos a editar el archivo composer.json
del proyecto shoppingcart
para que quede de la siguiente manera:
{
"name": "coppel/shoppingcart",
"description": "Online store",
"authors": [
{
"name": "Juan Perez",
"email": "jperez@coppel.com"
}
],
"require": {},
"autoload": {
"psr-4": {
"Coppel\\Yelu\\": "../yelu/src/"
}
}
}
Como se pueden dar cuenta, se agrego la propiedad autoload
y dentro de ella otra propiedad, psr-4
. Con esto le indicamos a composer que realice la auto carga utilizando el estándar PSR-4
. Ahora, dentro de la propiedad psr-4
es donde vamos a definir en que directorio la auto carga deberá buscar las clases cuando determinado namespace sea utilizado.
Continuando con el proyecto y para realizar un ejemplo práctico vamos a tomar la definición anterior donde indicamos que cuando se necesite utilizar o cargar una clase que contenga el namespace Coppel\Yelu
esta se deberá buscar un directorio atrás del directorio actual, seguido por un directorio yelu
y un directorio src
.
Para que composer actualice su configuración con los cambios de auto carga que hemos realizado es necesario ejecutar el comando composer dump-autoload
en la terminal.
Antes de actualizar la configuración, el archivo vendor/composer/autoload_psr4.php
contenía lo siguiente:
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);
Y después de ejecutar el comando el archivo contiene esto:
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Coppel\\Yelu\\' => array($baseDir . '/../yelu/src'),
);
La magia de la auto carga se puede explicar de una manera muy sencilla. Tenemos el archivo autoload_psr4.php
en el directorio shoppingcart/vendor/composer/
el cual contiene una variable de nombre $vendorDir
a la cual le es asignado el valor dirname(dirname(__FILE__))
el cual asigna el valor shoppingcart/vendor
ya que el directorio del archivo autoload_psr4.php
es shoppingcart/vendor/composer
y el directorio de este directorio es shoppingcart/vendor/
. Ahora la variable $baseDir
contiene el valor asignado por dirname($vendorDir)
el cual seria shoppingcart/
. Entonces, siguiendo con la parte de la auto carga, en el arreglo de retorno tenemos el key
Coppel\\Yelu\\
al cual se le asigna el valor de un arreglo conteniendo un único valor $baseDir
mas ../yelu/src/'
el cual resulta con la ruta shoppingcart/../yelu/src
que dirige al directorio yelu/src
que se encuentra un directorio atrás del directorio shoppingcart
.
Vamos a ejecutar nuevamente el comando php index.php
en la terminal para ver que es lo que sucede.
Ahora vemos que la clase si fue encontrada y como resultado en la terminal se mostrará el texto Coppel\Yelu\Yelu::__construct
.
Para cuestiones de una impresión de contenido limpia en la terminal, podemos eliminar la instrucción
echo
del constructor, de otra manera a medida que vayamos creando mas clases tendremos mas contenido innecesario imprimiéndose y podrá darse el caso se no identificar lo que necesitamos por todo el contenido impreso.
Ventajas de utilizar la auto carga
- No es necesario especificar un
require
oinclude
cada vez que necesitemos de una clase. Pero será necesario especificar el namespace mediante la palabra reservadause
. - Nos olvidamos por completo donde se encuentran los archivos de clase, si estos cambian de ubicación solo se necesita actualizar las referencias en el archivo
composer.json
y ejecutar el comandocomposer dump-autoload
para actualizar las rutas.
Muchas veces es difícil recordar la ruta donde se encuentra cada archivo. Es mas fácil recordar un namespace tal como Coppel\Database\PostgreSql
que incluir un archivo PHP con una ruta tal como../../framework/conexiones/postgresql.php
que puede cambiar dependiendo del directorio donde se encuentre el archivo desde el que la estamos requiriendo.
Definiendo la configuración
Antes de comenzar a escribir controladores, modelos y vistas vamos a definir la configuración que vamos a utilizar en el proyecto y vamos a pasarla al framework para que este administre esa configuración y defina todas las características a utilizar a como se encuentren definidas en el proyecto.
Los datos de configuración los vamos a definir en un arreglo, que a su vez contendrá otros sub-arreglos con configuración mas especifica. Vamos a crear el archivo App.php
dentro del directorio src/config
en el proyecto shoppingcart
. Dentro de el vamos a ingresar lo siguiente:
<?php
return [
];
Directorio base
Cuando pasemos la configuración al framework, este necesita saber con que proyecto va a trabajar, o de otra manera, necesita conocer donde se encuentran los archivos con los que va interactuar, por ello necesitamos especificar la ruta absoluta donde se encuentra el proyecto. Vamos a agregar lo siguiente al arreglo de configuración config/App.php
.
<?php
return [
"baseDir" => dirname(__DIR__)
];
Con esto, estamos asignando a la llave baseDir
la ruta absoluta del directorio src
del proyecto shoppingcart
.
Vamos a realizar una prueba para observar cual es el valor asignado a la variable baseDir
, agreguemos lo siguiente al final del archivo index.php
del proyecto var_dump(require_once './src/config/App.php');
y ejecutemos el archivo index.php
con el interprete de PHP en la terminal.
Deberia aparecer algo como lo siguiente:
array(1) {
["baseDir"]=>
string(58) "/home/sergio/Projects/shoppingcart/src"
}
Inicializando el framework
Ya hemos definido un valor de configuración en el proyecto shoppingcart
, ahora necesitamos pasar esa configuración al framework para que este comience a interpretarla y haga algo con ella.
Vamos a editar la clase Yelu
contenida el archivo src/Yelu.php
del framework para agregar un nuevo método público de nombre init
, el cual deberá requerir un parámetro de nombre $config
, a la vez necesitamos definir una propiedad privada con el mismo nombre.
<?php
namespace Coppel\Yelu;
class Yelu
{
private $config;
public function __construct()
{
# echo __METHOD__;
}
public function init($config)
{
$this->config = $config;
}
}
Ahora en el archivo index.php
del proyecto shoppingcart
vamos a pasar el arreglo de configuración al método init
de la clase Yelu
.
<?php
require_once "./vendor/autoload.php";
$yelu = new Coppel\Yelu\Yelu();
$yelu->init(require_once './src/config/App.php');
Hecho esto, cualquier configuración que definamos en el archivo App.php
será pasada al framework.
Controladores
Los controladores son los puntos de entrada principal para el proyecto, cuando una petición llega al servidor, el framework deberá de encargarse de redirigirla hacia el controlador con su método indicado.
Para comenzar vamos a definir la configuración de los controladores, lo que necesitamos es darle a conocer al framework el namespace que un controlador deberá tener. Definiendo esto y con ayuda de la auto carga de composer fácilmente podemos ubicar una clase tipo controlador para ser cargada.
Vamos a crear el archivo Namespaces.php
en el directorio config
del proyecto shoppingcart
, en el agregaremos el siguiente contenido:
<?php
return [
"controller" => "Coppel\\Controllers\\"
];
Como pueden ver, para los controladores estamos definiendo un namespace Coppel\Controllers
y como el archivo de configuración principal es config/App.php
deberemos cargarlo desde ahí. Hay que editar el archivo config/App.php
para que quede de la siguiente manera:
<?php
return [
"baseDir" => dirname(__DIR__),
"namespaces" => require_once "./src/config/Namespaces.php"
];
Ahora la configuración deberá ser como lo siguiente:
array(2) {
["baseDir"]=>
string(58) "/home/sergio/Projects/shoppingcart/src"
["namespaces"]=>
array(1) {
["controller"]=>
string(19) "Coppel\Controllers\"
}
}
Agregando la ruta de los controladores a la auto carga
Para que la auto carga sepa como cargar un controlador deberemos especificarlo a como lo hicimos con el framework. Hay que editar el archivo composer.json
y agregar lo siguiente a la propiedad psr-4
.
"Coppel\\Controllers\\": "src/controllers/"
Cuando se necesite cargar una clase de tipo controlador (esto lo sabemos por el namespace Coppel\Controllers
) la auto carga la buscará en el directorio src/controllers
del proyecto shoppingcart
.
Pro Tip: Cada vez que se edite el archivo composer.json_ y se agregue una nueva referencia a la propiedad __psr-4 es necesario ejecutar el comando composer dump-autoload en la terminal para reflejar los cambios.
Cargando el controlador en el framework
En el método init
de la clase Yelu
vamos a crear una instancia de un controlador, para ello primero necesitamos crear uno en el directorio scr/controllers
, vamos a llamarlo Home.php
y contendrá lo siguiente:
<?php
namespace Coppel\Controllers;
class Home
{
public function __construct()
{
echo __METHOD__;
}
}
Ahora si, editemos la clase Yelu
para agregar la carga del controlador:
<?php
namespace Coppel\Yelu;
class Yelu
{
private $config;
public function __construct()
{
}
public function init($config)
{
$this->config = $config;
$className = "{$this->config['namespaces']['controller']}Home";
$home = new $className();
}
}
Como se puede observar, el nombre de la clase a cargar (controlador) es la unión del namespace de los controladores mas el nombre de la clase, cuando una clase se encuentra contenida en un namespace su nombre pasa a ser el namespace mas el nombre de clase y en este caso el nombre de la clase Home
es Coppel\Controllers\Home
.
Si ejecutamos el archivo index.php
con PHP tendremos el siguiente resultado en la consola Coppel\Controllers\Home::__construct
.
Ruteo de URL's
Cuando hablamos de ruteo en un framework lo que se está haciendo es tomar la dirección url de la petición que llega al servidor y de acuerdo a ciertos criterios determinar que acción deberá ejecutarse. En este caso deberemos de determinar que método de que controlador vamos a llamar.
Vamos a crear el archivo Router.php
dentro del directorio src
del framework y dentro de el crearemos la clase Router
.
<?php
namespace Coppel\Yelu;
class Router
{
public function __construct()
{
}
}
Determinar la ruta
Una configuración de ruta mínimamente debe contar con dos elementos, el método HTTP y el Path, los parámetros son otro elemento muy importante pero no necesario para determinar una ruta, mas adelante veremos como utilizarlos.
El arreglo super global $_SERVER
contiene los dos elementos que necesitamos y están especificados con las llaves REQUEST_URI
y REQUEST_METHOD
. Vamos a extraerlos en el constructor de la clase para observar los valores que contienen cuando una petición es lanzada al servidor.
<?php
namespace Coppel\Yelu;
class Router
{
public function __construct()
{
$path = filter_input(INPUT_SERVER, 'REQUEST_URI');
$method = filter_input(INPUT_SERVER, 'REQUEST_METHOD');
var_dump($path, $method);
}
}
En el método init
de la clase Yelu
vamos a crear una instancia de la clase Router
y llamaremos el archivo index.php
con PHP. Como resultado en la terminal tendremos un resultado un poco extraño:
NULL
NULL
Y bueno, ¿por qué sucede esto?
Como estamos ejecutando el script de PHP directamente desde el interprete, no desde un navegador web, no tenemos un método y una url. Necesitamos ejecutar el script con un servidor web, para ello podemos ejecutar el build-in server que viene integrado con PHP para poder acceder por medio de un navegador y poder lanzar peticiones HTTP.
php -S localhost:8888
Ahora desde Postman
vamos a llamar la url http://localhost:8888
con el método GET
, el resultado será:
string(1) "/"
string(3) "GET"
Inclusive, si accede a la url desde el navegador también podrá obtener el mismo resultado.
Intente lanzar mas peticiones agregando rutas a la url y cambiando el método HTTP, con ello verá que los valores también cambiarán. Utilize
Postman
ya que desde el navegador no puede cambiar el método HTTP
Ahora que podemos obtener el método HTTP y el path hay que almacenarlos para poder utilizarlos en el ruteo.
<?php
namespace Coppel\Yelu;
class Router
{
private $method;
private $path;
public function __construct()
{
$this->path = filter_input(INPUT_SERVER, 'REQUEST_URI');
$this->method = filter_input(INPUT_SERVER, 'REQUEST_METHOD');
}
}
Configurar las rutas de la aplicación
Ya que tenemos almacenados los dos datos que necesitamos para realizar un ruteo debemos de configurar las rutas con las que trabajará la aplicación. Hay que crear el archivo de configuración Routes.php
en el proyecto shoppingcart
y agregar una ruta que dirija al controlador Home
.
<?php
return [
[
"method" => "GET",
"path" => "/"
]
];
También hay que incluirlo en el archivo App.php
.
<?php
return [
"baseDir" => dirname(__DIR__),
"namespaces" => require_once "./src/config/Namespaces.php",
"routes" => require_once "./src/config/Routes.php"
];
Validar la ruta
Ya que tenemos configurada por lo menos una ruta en la configuración vamos a validar si cuando se accede al proyecto la ruta existe o no. Hay que pasar el arreglo de rutas a la clase Router
, pero vamos a hacerlo en un nuevo método público de nombre start
donde haremos la validación.
<?php
namespace Coppel\Yelu;
class Router
{
private $method;
private $path;
public function __construct()
{
$this->path = filter_input(INPUT_SERVER, 'REQUEST_URI');
$this->method = filter_input(INPUT_SERVER, 'REQUEST_METHOD');
}
public function start($routes)
{
}
}
<?php
namespace Coppel\Yelu;
class Yelu
{
private $config;
public function __construct()
{
}
public function init($config)
{
$this->config = $config;
$router = new Router();
$router->start($this->config['routes']);
}
}
Dentro del método start
vamos a comparar cada par de método y ruta configurado en el arreglo de rutas con el método y ruta que llegaron al servidor y mostrar un mensaje que nos haga saber si la ruta existe o no.
<?php
public function start($routes)
{
foreach ($routes as $route) {
if ($route['method'] === $this->method & $route['path'] === $this->path) {
echo "La ruta $this->method $this->path existe!";
} else {
echo "La ruta $this->method $this->path NO existe!";
}
}
}
Ahora podemos comenzar a lanzar peticiones al servidor y validar si la ruta y método a la que estamos accediendo se encuentran configuradas.
Puedes agregar mas rutas al arreglo para realizar la prueba, trata de cambiar tanto el path como el método.
Cargar el controlador y método de una ruta
Ya hemos definido rutas y métodos en la configuración del ruteo.
¿Pero cómo sabemos a que controlador/método llamar?
Es necesario especificar para cada ruta cual es el controlador y el método que llamaremos.
Al controlador Home
hay que agregarlo un método público de nombre homePage
que imprima algo.
<?php
public function homePage()
{
echo __METHOD__;
}
Ahora vamos a agregar la clase y método que llamaremos a la ruta.
<?php
return [
[
"method" => "POST",
"path" => "/",
"handler" => [
"class" => "Home",
"method" => "homePage"
]
]
];
En la validación del ruteo, cuando la ruta exista cargaremos el controlador y llamaremos el método.
<?php
public function start($routes)
{
foreach ($routes as $route) {
if ($route['method'] === $this->method &
$route['path'] === $this->path) {
$class = $route['handler']['class'];
$handler = new $class();
} else {
echo "La ruta $this->method $this->path NO existe!";
}
}
}
Lancemos una petición al servidor de la forma GET /
, como resultado obtendremos un error como el siguiente Class 'Home' not found in /home/sergio/Projects/yelu/src/Router.php on line 26
. Esto es por que estamos tratando de crear una instancia de la clase Home
sin un namespace, recordemos que en la configuración definimos el namespace que tendrán los controladores y necesitamos incluirlo al momento de crear la clase.
Para hacer esto, vamos a pasar el valor del namespace de los controladores como parámetro en el método start
.
<?php
public function init($config)
{
$this->config = $config;
$router = new Router();
$router->start($this->config['routes'], $this->config['namespaces']['controller']);
}
<?php
public function start($routes, $controllerNamespace)
{
foreach ($routes as $route) {
if ($route['method'] === $this->method &
$route['path'] === $this->path) {
$class = "{$controllerNamespace}{$route['handler']['class']}";
$handler = new $class();
} else {
echo "La ruta $this->method $this->path NO existe!";
}
}
}
Ahora si podemos realizar una prueba desde Postman
o desde un navegador web, inclusive podemos cambiar el contenido del método homePage
para mostrar algo diferente.
Un poco de información
Como hemos visto en este último tema hemos utilizado ciertas combinaciones de texto para crear la instancia de la clase y llamar al método. Existen muchas maneras de hacerlo aparte de la forma anterior.
<?php
# Las dos sentencias producen el mismo resultado
$class = "{$controllerNamespace}{$route['handler']['class']}";
$class = $controllerNamespace . $route['handler']['class'];
# Las tres sentencias producen el mismo resultado
$handler->$route['handler']['method']();
call_user_func_array(array($handler, $route['handler']['method']), []);
call_user_func(array($handler, $route['handler']['method']), []);
Vistas
Las vistas son pequeños fragmentos o secciones de una página (generalmente elementos HTML) que pueden ser cargados desde un controlador para ser mostrados como salida, esta salida puede ser modificada con la ayuda de variables de contexto que harán que una vista no sea un simple contenido estático.
Vamos a realizar la carga de vistas desde un controlador, para ello necesitamos contar con cierta información como el nombre de la vista a cargar, la ubicación o las variables de contexto, pero para no realizar estas tareas manualmente en cada controlador vamos a crear un controlador base
el cual realice estas operaciones y desde los demás controladores podamos utilizarlas sin necesidad de reescribir las tareas en cada uno.
Definiendo el constructor base
Crearemos el archivo BaseController.php
en el directorio src
del framework. Dentro de el crearemos la clase base.
<?php
namespace Coppel\Yelu;
class BaseController
{
public function __construct()
{
echo __METHOD__;
}
}
Ahora vamos a extender
o heredar
la clase Home
desde BaseController
.
<?php
namespace Coppel\Controllers;
use Coppel\Yelu\BaseController;
class Home extends BaseController
{
public function __construct()
{
}
public function homePage()
{
echo "Hello world!";
}
}
Si accedemos nuevamente desde el navegador a la ruta GET /
veremos que nada ha cambiado. Hay que notar algo, cuando definimos el constructor de la clase BaseController
declaramos una sentencia echo __METHOD__
.
¿Y por qué no se muestra?
Cuando heredas o extiendes una clase de otra los constructores no son llamados automáticamente, de ser necesario se deben llamar manualmente de la siguiente manera parent::__construct()
.
Pro Tip: los destructores son llamados de la manera
parent::__destruct()
y tanto los constructores como los destructores pueden llamarse en cualquier método, no necesariamente en el constructor o destructor de la clase.
Método render
Dentro de la clase BaseController
agregaremos un método público de nombre render
que contará con un parámetro de nombre $view
la cual hará la búsqueda y carga de la vista indicada.
Antes de comenzar a escribir este método es necesario configurar la ruta donde se estarán ubicadas las vistas. Vamos a agregar lo siguiente al arreglo de configuración "viewsDir" => "./src/views/"
.
<?php
return [
"baseDir" => dirname(__DIR__),
"viewsDir" => "./src/views/",
"namespaces" => require_once "./src/config/Namespaces.php",
"routes" => require_once "./src/config/Routes.php"
];
Ahora necesitamos realizar un cambio a la manera como accedemos a la configuración para que sea accesible desde cualquier clase dentro del proyecto y del framework.
Vamos a cambiar la propiedad $config
de la clase Yelu
a static
y eliminaremos los parámetros del método start
la clase Router
. Además, vamos a agregar una conversión para utilizar el arreglo de configuración con el operador de acceso a propiedades de objetos en lugar de tener que estar accediendo a los valores por medio de los corchetes y comillas.
<?php
namespace Coppel\Yelu;
class Yelu
{
public static $config;
public function __construct()
{
}
public function init($config)
{
self::$config = json_decode(json_encode($config), false);
$router = new Router();
$router->start();
}
}
<?php
public function start()
{
foreach (Yelu::$config->routes as $route) {
if ($route->method === $this->method &
$route->path === $this->path) {
$class = Yelu::$config->namespaces->controller . $route->handler->class;
$handler = new $class();
$method = $route->handler->method;
$handler->$method();
} else {
echo "La ruta $this->method $this->path NO existe!";
}
}
}
Vamos a crear el método render
en la clase BaseController
y la vista que pasemos como parámetro vamos a incluirla con la instrucción require_once
, agregando al inicio el directorio de las vistas.
<?php
namespace Coppel\Yelu;
class BaseController
{
public function __construct()
{
}
protected function render($view)
{
require_once Yelu::$config->viewsDir . $view;
}
}
Dentro del directorio src/views
vamos a agregar una vista de nombre home.php
la cual tendrá el siguiente contenido:
<h1>¡Bienvenido!</h1>
Y en el controlador Home
vamos a cargar la vista de la siguiente manera:
<?php
namespace Coppel\Controllers;
use Coppel\Yelu\BaseController;
class Home extends BaseController
{
public function __construct()
{
}
public function homePage()
{
$this->render('home.php');
}
}
¿Incluir la extensión de las vistas estaría de mas no?
Vamos a corregir esto cambiando la siguiente línea en la clase BaseController
: require_once Yelu::$config->viewsDir . $view;
por require_once Yelu::$config->viewsDir . $view . ".php";
y al cargar la vista eliminamos el .php
.
Con esto simplemente cargamos las vistas con su respectivo nombre, y como en realidad lo que estamos haciendo es especificar un nombre de archivo para generar una ruta absoluta al archivo de vista fácilmente podemos crear un sub-directorio en el directorio de vistas para crear grupos y cargarlas de la manera $this->render('subfolder/items');
Entonces vamos a actualizar la página en el navegador para poder ver un encabezado con el texto ¡Bienvenido!
o desde Postman
para ver el código fuente <h1>¡Bienvenido!</h1>
.
Creando layouts
Un layout en terminos de vistas y plantillas es una plantilla principal de la cual pueden extender las vistas, es decir, podemos utilizar un layout para definir la mayoria de los elementos de un sitio web y en ciertas partes mas pequeñas o de contenido dínamico podemos incluir el contenido de las vistas. Un layout es un elemento re-utilizable.
Controlado la salida de la información
Este tema puede ser muy extenso y vamos a utilizar unas cuantas funciones referentes al output buffering para preservar la salida de información, hacer algo con ella y cuando sea necesario enviarla para que sea mostrada.
Si en cierto momento utilizamos una instrucción require
o include
, inmediatamente el contenido de los archivos que incluyamos será procesado por PHP y si en uno de esos archivos tenemos alguna instrucción que envie algo como salida este será enviado al momento.
Toda la información referente al control de salida se encuentra disponible en la documentación de PHP.
Hemos utilizado las instrucciones require
o include
para asignar el retorno de un archivo incluido a una variable, el archivo de configuración App.php
es uno de ellos y si hechamos un vistazo al código veremos que podemos realizar algo como esto:
<?php
$contenido = require_once "App.php";
var_dump($contenido);
El contenido del archivo App.php
es asignado a la variable por que dentro de el tenemos una instrucción return
y si tuvieramos algo como un echo
, var_dump
o print
antes del return
este seria enviado como salida y mostrado al momento en el navegador, intentalo y veras.
Con las vistas no podemos hacerlo de esta manera ya que el contenido realmente es salida que necesitamos mostrar y no una variable PHP, claro, pudieramos escribir todas las salidas utilizando strings
pero eso seria una muy mala practica.
De echo, hasta el momento lo tenemos de esta precisa manera:
<?php
require_once Yelu::$config->viewsDir . $view . ".php";
Estamos utilizando un require_once
para incluir la vista, perfecto.
¿Pero como le hacemos para trabajar con el layout y que esa vista sea incluida en una sección entre todo el contenido del layout? La respuesta es almacenando esa salida hasta que la necesitemos.
Vamos a crear un nuevo método privado en la clase BaseController
y le llamaremos internalRender
, este método aceptará un parámetro de nombre $view
y en el método render
llamaremos a internalRender
pasando la vista que se necesita cargar.
<?php
namespace Coppel\Yelu;
class BaseController
{
public function __construct()
{
}
protected function render($view)
{
$this->internalRender($view);
}
private function internalRender($view)
{
ob_start();
include_once "Yelu::$config->viewsDir$view.php";
$content = ob_get_contents();
ob_end_clean();
echo $content;
}
}
Si recargamos el navegador veremos que no aparece nada y es por que cuando realizamos el armado de la cadena de la ruta del achivo en realidad lo PHP hace es retornar algo como esto Yelu::Home.php
y no ".src/views/Home.php
. Hay un problema al escapar los valores de una variable estática en un string con comillas dobles, no es posible hacerlo, PHP entiende que en la expresión "Yelu::$config->viewsDir$view.php"
, la variable a escapar es $config->viewsDir
sin tomar en cuenta que es una variable estática de la claseYelu
.
Vamos a cambiar la expresión include_once "Yelu::$config->viewsDir$view.php";
por lo siguiente:
<?php
$viewsDir = Yelu::$config->viewsDir;
include_once "$viewsDir$view.php";
Recarguemos el navegador y veremos que nuevamente aparecerá el texto.
¿Cómo funciona todo eso que hay dentro del método internalRender
? Al iniciar el método ejecutamos la función ob_start()
la cual a partir de ese momento inicializa la captura del bufer de salida (object buffer start), toda información enviada como salida despues de esta instrucción no será enviada directamente como salida en el navegador, será almacenada hasta que le indiquemos ser mostrada o descartada. Luego incluimos el archivo de vista sin ser mandado como salida siguiendo con la ejecución de la función ob_get_contents
(object buffer get contents) que a como su nombre indica, obtiene el contenido del bufer de salida para asignarlo a una variable y por último ejecutamos la función ob_end_clean
la cual detiene la captura del bufer de salida y limpia el contenido almacenado en el. Hay que notar que antes de limpiar el contenido del bufer debemos de obtenerlo, seria ilogico el limpiar el bufer y al final tratar de obtener su contenido.
Pero yo sigo mirando el mismo resultado, ¿qué ha cambiado?
Lo que ha cambiado es que ahora el contenido de las vistas está siendo almacenado en una variable, algo que antes no podiamos realizar, y para darle mas sentido a la carga del layout en lugar de imprimir el contenido directamente vamos a retornarlo, cambiemos echo $content;
al final del método por return $content;
.
Mostrando el archivo Layout
Vamos a crear el layout en el directorio de vistas, le daremos el nombre layout.php
y su contenido será el siguiente:
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
<meta charset="utf-8">
</head>
<body>
<h1>Layout</h1>
</body>
</html>
En el método render
de la clase BaseController
vamos incluir la vista layout e imprimirla, también lo haremos con la vista y recargaremos el navegador para ver el resultado. El nombre de la vista lo vamos a obtener de un nuevo valor de la configuración que llamaremos layout
.
<?php
return [
"baseDir" => dirname(__DIR__),
"viewsDir" => "./src/views/",
"layout" => "layout",
"namespaces" => require_once "./src/config/Namespaces.php",
"routes" => require_once "./src/config/Routes.php",
"database" => require_once "./src/config/Database.php"
];
<?php
protected function render($view)
{
echo $this->internalRender(Yelu::$config->layout);
echo $this->internalRender($view);
}
¡Perfecto!, ahora vemos que tenemos las dos salidas, el contenido del archivo layout.php
y la vista.
Sigo pensando que con un simple
require "layout.php"
einclude "$view.php"
hubiera sido suficiente para mostrar el contenido.
Claro, pudimos haber incluido los archivos directamente, pero antes echemos un vistazo al código fuente en el navegador.
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
<meta charset="utf-8">
</head>
<body>
<h1>Layout</h1>
</body>
</html>
<h1>¡Bienvenido!</h1>
De alguna manera el <h1>¡Bienvenido!</h1>
que es el contenido de la vista no debiera de aparecer ahí. Deberíamos hacer algo para que ese contenido se encuentre dentro de las etiquetas body
del código HTML.
Utilizando variables de contexto
Algo que podemos hacer para resolver la situación anterior es almacenar el contenido de la vista a mostrar en una variable y pasarla a la vista layout para mostrar su contenido donde sea necesario.
Para ello deberemos de procesar el contenido de la vista primero y almacenarlo en una variable, luego modificar el método internalRender
para pasar las variable de contexto y hacerlas accesibles en el archivo de layout.
<?php
namespace Coppel\Yelu;
class BaseController
{
public function __construct()
{
}
protected function render($view)
{
$content = $this->internalRender($view);
echo $this->internalRender(Yelu::$config->layout, $content);
}
private function internalRender($view, $context = [])
{
ob_start();
$viewsDir = Yelu::$config->viewsDir;
include_once "$viewsDir$view.php";
$content = ob_get_contents();
ob_end_clean();
return $content;
}
}
Hemos agregado una asignación de un valor predeterminado al parámetro $context
ya que si no es necesario pasar variables de contexto podemos omitir el parámetro sin problemas.
Ya tenemos el contenido de la vista en una variable y esa variable está disponible al momento de cargar la vista.
¿Pero cómo hacemos que esa variable esté disponible en el contexto de la vista layout?
Extract es una función que pertenece a la categoría de los arreglos y lo que hace es importar variables al contexto actual, o dicho de otro modo, crear variables a partir de los valores de un arreglo. Por ejemplo:
<?php
$datos = [
"hola" => "mundo!",
"cantidad" => 12,
"verdadero" => true
];
extract($datos);
var_dump($hola, $cantidad, $verdadero);
El resultado del código anterior es el siguiente:
string(6) "mundo!"
int(12)
bool(true)
Como pueden ver, nunca declaramos las variables $hola
, $cantidad
y $verdadero
, solo eran valores contenidos en el arreglo $datos
y la función extract
creo las variables a partir de los datos del arreglo.
Note que al utilizar la función
extract
la llave del elemento en el arreglo es el nombre que le dará a la variable al momento de crearla.
Si ejecutáramos el código anterior sin el uso de la función extract
obtendríamos errores de variables sin definir.
PHP Notice: Undefined variable: hola in /script.php on line 11
PHP Notice: Undefined variable: cantidad in /script.php on line 11
PHP Notice: Undefined variable: verdadero in /script.php on line 11
NULL
NULL
NULL
Ahora que podemos crear variables de manera dinámica vamos a pasar el contenido de la vista a un arreglo y le daremos el nombre o llave content
, luego pasaremos el arreglo como parámetro de contexto al método internalRender
y por último antes de incluir la vista y después de inicializar la captura del bufer de salida vamos a extraer las variables al contexto de la vista.
<?php
namespace Coppel\Yelu;
class BaseController
{
public function __construct()
{
}
protected function render($view)
{
$content = $this->internalRender($view);
$context = [
"content" => $content
];
echo $this->internalRender(Yelu::$config->layout, $context);
}
private function internalRender($view, $context)
{
ob_start();
extract($context);
$viewsDir = Yelu::$config->viewsDir;
include_once "$viewsDir$view.php";
$content = ob_get_contents();
ob_end_clean();
return $content;
}
}
Si recargamos el navegador veremos que solo aparece el contenido del archivo layout.php
pero el de la vista no. Nos ha faltado imprimir el contenido de la variable recién creada de nombre $content
en la vista del layout.
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
<meta charset="utf-8">
</head>
<body>
<h1>Layout</h1>
<?= $content ?>
</body>
</html>
Ahora si, al actualizar el contenido del navegador aparecerá el contenido de la vista y si observamos el código fuente veremos que el <h1>¡Bienvenido!</h1>
aparece justamente donde imprimimos la variable $content
.
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
<meta charset="utf-8">
</head>
<body>
<h1>Layout</h1>
<h1>¡Bienvenido!</h1>
</body>
</html>
Assets
Para darle un diseño decente y de manera rápida a la página vamos a utilizar el famoso framework de front-end Bootstrap
. Para ello necesitamos que la página sea capaz de cargar archivos de CSS
y Javascrit
y actualmente, con el build-in server
se puede hacer.
Vamos a extraer el contenido del archivo bootstrap-3.3.4-jquery-1.11.3-dist.zip
dentro del directorio assets
en el proyecto shppingcart
y vamos a realizar unos cambios en la vista layout para hacer referencia a esos archivos.
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
<meta charset="utf-8">
<link href="/assets/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
<h1>Layout</h1>
<?= $content ?>
<script src="/assets/js/jquery-1.11.3.js" rel="stylesheet"></script>
<script src="/assets/js/bootstrap.js" rel="stylesheet"></script>
</body>
</html>
Al recargar el navegador veremos que el texto a tomado otro estilo, esto es gracias a Bootstrap
.
De una vez agregaremos unas etiquetas que utilizaran las clases de Bootstrap
para formar el contenido de la página a como lo vamos a necesitar.
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
<meta charset="utf-8">
<link href="/assets/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">Shopping Cart</a>
</div>
</div>
</div>
<div class="container">
<div class="row">
<?= $content ?>
</div>
</div>
<script src="/assets/js/jquery-1.11.3.js" rel="stylesheet"></script>
<script src="/assets/js/bootstrap.js" rel="stylesheet"></script>
</body>
</html>
Recargamos el navegador para ver que ahora hay una barra de navegación en la parte superior con el texto Shopping Cart
del lado izquierdo.
¿Notan que el contenido de la vista queda por debajo de la barra?
Podemos corregir ese detalle agregando unas reglas de CSS
, hay que crear un nuevo archivo de nombre shoppingcart.css
dentro del directorio assets/css
y agregar el siguiente contenido:
body {
margin-top: 60px;
}
Ahora hay que agregar la referencia al archivo de CSS
en el layout agregando la siguiente linea antes de cerrar la etiqueta head
:
<link href="/assets/css/bootstrap.css" rel="stylesheet" />
Con esto podemos realizar el siguiente cambio a la vista home
para que el mensaje de bienvenida luzca mejor.
<div class="col-md-12">
<div class="jumbotron">
<h1>Bienvenido</h1>
</div>
</div>
Modelos
Vamos a crear un nuevo controlador de nombre Items
el cual deberá cargar una nueva vista de nombre list
ubicada dentro de un sub-directorio de nombre items
, la carga se hará dentro de un nuevo método público de nombre listPage
.
El contenido del controlador Items
es el siguiente:
<?php
namespace Coppel\Controllers;
use Coppel\Yelu\BaseController;
class Item extends BaseController
{
public function __construct()
{
}
public function listPage()
{
$this->render('items/list');
}
}
Y el contenido de la vista list
es:
<h1>Lista de artículos</h1>
Aquí hemos creado el archivo
list.php
en la rutasrc/views/items
, como puede ver el archivo se creo en un sub-directorio, por lo cual es necesario especificar ese directorio al momento de cargarlo. Por ejemplo:$this->render('items/list');
.
El método listPage
deberá ejecutarse cuando se acceda a la ruta GET /items
por medio del navegador, por lo cual hay que agregar la siguiente ruta al archivo de configuración de rutas:
<?php
return [
[
"method" => "GET",
"path" => "/",
"handler" => [
"class" => "Home",
"method" => "homePage"
]
],
[
"method" => "GET",
"path" => "/items",
"handler" => [
"class" => "Item",
"method" => "listPage"
]
]
];
Accedemos a la ruta /items
en el navegador para ver el contenido y nos daremos cuenta de que aparte del contenido de la vista list
aparece un texto con el contenido La ruta GET /items NO existe!
.
Esto es por que cuando realizamos la validación de las rutas en el método start
de la clase Router
tenemos una condición la cual valida por cada una de las rutas que existen en el arreglo de configuración de rutas si el método y el path concuerdan con los que llegaron al servidor, de no concordar enviamos un mensaje indicando que la ruta no existe. El problema es que como inicialmente teníamos una sola ruta no habia problema ya que o ejecutaba el método de la clase si existia o mostraba el mensaje indicando que la ruta no existe.
Vamos a cambiar ese mensaje para mostrarlo solo cuando realmente la ruta no exista y no cada vez que una ruta no exista dentro de la instrucción foreach
.
<?php
public function start()
{
$routeExists = false;
foreach (Yelu::$config->routes as $route) {
if ($route->method === $this->method &
$route->path === $this->path) {
$routeExists = true;
$class = Yelu::$config->namespaces->controller . $route->handler->class;
$handler = new $class();
$method = $route->handler->method;
$handler->$method();
break;
}
}
if (!$routeExists) {
echo "La ruta $this->method $this->path NO existe!";
}
}
Igualmente que para los controladores, vamos a crear una clase BaseModel
la cual implemente funcionalidades básicas para los modelos y nos sirva para extender de ella cada nuevo modelo que utilicemos. El archivo donde estará esta clase se llamará BaseModel.php
y estará ubicado en el directorio src
del framework.
<?php
namespace Coppel\Yelu;
class BaseModel
{
public function __construct()
{
}
}
Una de las principales funciones del modelo base será generar una conexión a base de datos, los datos de conexión deberá tomarlos desde un nuevo archivo de configuración de nombre Database.php
el cual contendrá lo siguiente:
<?php
return [
"host" => "localhost",
"database" => "shoppingcart",
"user" => "root",
"password" => ""
];
De igual manera, hay que agregarlo al archivo de configuración principal:
<?php
return [
"baseDir" => dirname(__DIR__),
"viewsDir" => "./src/views/",
"namespaces" => require_once "./src/config/Namespaces.php",
"routes" => require_once "./src/config/Routes.php",
"database" => require_once "./src/config/Database.php"
];
Ahora vamos a crear le método protegido getConnection
en la clase BaseModel
, el cual obtendrá los datos de configuración para crear una conexión a base de datos y retornarla
protected function getConnection()
{
$host = Yelu::$config->database->host;
$dbname = Yelu::$config->database->database;
$user = Yelu::$config->database->user;
$password = Yelu::$config->database->password;
$dsn = "mysql:host=$host;dbname=$dbname";
return new \PDO(
$dsn,
$user,
$password,
[
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
]
);
}
Con esto podemos obtener una conexión a base de datos si extendemos un modelo de esta clase. Hay que crear el modelo Item
en el directorio src/models
en el proyecto shoppingcart
<?php
namespace Coppel\Models;
use Coppel\Yelu\BaseModel;
class Item extends BaseModel
{
public function __construct()
{
}
}
De acuerdo al modelo MVC, deberemos poder cargar un modelo desde los controladores, para ello vamos a crear un nuevo método protegido de nombre getModel
en la clase BaseController
el cual debe aceptar un parámetro de nombre $modelName
.
Recordemos que para cargar un controlador hicimos uso del namespace al que pertenecen los controladores, para los modelos haremos lo mismo. Hay que configurar el namespace para los modelos dentro de la configuración de namespaces.
<?php
return [
"controller" => "Coppel\\Controllers\\",
"model" => "Coppel\\Models\\"
];
También deberemos agregarlo a la configuración de composer y posteriormente ejecutar el comando composer dump-autoload
, el valor que vamos a agregar es "Coppel\\Models\\": "src/models/"
.
Ahora vamos a cargar el modelo de una manera similar a como cargamos los controladores.
protected function getModel($modelName)
{
$model = Yelu::$config->namespaces->model . $modelName;
return new $model();
}
Y en el método listPage
del controlador Item
cargamos el modelo.
public function listPage()
{
$itemModel = $this->getModel('Item');
$this->render('items/list');
}
Obteniendo los artículos
En el modelo Item
vamos a agregar un nuevo método público de nombre getItems
el cual deberá retornar un arreglo con todos los artículos que cuente la tabla items
de la base de datos shoppingcart
.
public function getItems()
{
$dbh = $this->getConnection();
$sth = $dbh->query('SELECT * FROM items;');
return $sth->fetchAll();
}
Y este método lo vamos a llamar desde el modelo que acabamos de cargar en el controlador para mostrar la información como salida.
public function listPage()
{
$itemModel = $this->getModel('Item');
$items = $itemModel->getItems();
var_dump($items);
$this->render('items/list');
}
Si recargamos el navegador veremos que aparecerá el arreglo con toda la información en la página. Y si revisamos el código fuente veremos que nuevamente tenemos una situación similar a la de cuando intentábamos mostrar el contenido de la vista en cierta parte del contenido del layout.
array(100) {
[0]=> ...
}
<!DOCTYPE html>
<html>
<head>
Entonces necesitamos mostrar el contenido del arreglo en cierta parte dentro de la vista que estamos utilizando, para ello deberemos de realizar lo mismo que hicimos para el layout, almacenar el contenido en una variable y pasarla como variable de contexto para la vista.
Vamos a modificar el método render
de la clase BaseController
y agregar el parámetro $locals
y de valor predeterminado asignaremos un arreglo vacio.
protected function render($view, $locals = [])
{
$context = [];
$context['content'] = $this->internalRender($view, $locals);
echo $this->internalRender(Yelu::$config->layout, $context);
}
Ahora deberemos de enviar el resultado del módelo como variable de contexto a la vista que estamos utlilzando.
public function listPage()
{
$itemModel = $this->getModel('Item');
$items = $itemModel->getItems();
$this->render('items/list', [
'items' => $items
]);
}
Como le hemos asignado el nombre items
en la vista deberemos de utilizar el mismo nombre para imprimir su valor.
<h1>Lista de artículos</h1>
<?php
var_dump($items);
?>
Ahora si refrescamos la página en el navegador podremos ver que la información sigue apareciendo pero ahora se encuentra después del título de la página.
Variables de contexto predeterminadas
Antes de proceder a darle un formato a todo el contenido que hemos obtenido de la base de datos vamos a definir unas variables de contexto que tomaran un valor predeterminado si no las especeficamos, un ejemplo de ellas es una variable que va a contener el titulo de la pagina actual. Vamos a crear un nuevo archivo de configuración de nombre Context.php
y en el vamos a definir las variables de contexto.
<?php
return [
"title" => "Shopping Cart"
];
Igualmente vamos a agregarlo a la configuración inicial.
<?php
return [
"baseDir" => dirname(__DIR__),
"viewsDir" => "./src/views/",
"layout" => "layout",
"namespaces" => require_once "./src/config/Namespaces.php",
"routes" => require_once "./src/config/Routes.php",
"database" => require_once "./src/config/Database.php",
"context" => require_once "./src/config/Context.php"
];
El funcionamiento que las variables de contexto predeterminadas van a tener es el siguiente: si una variable de contexto predeterminada existe, esta será pasada solamente al layout y no a las vistas como tal, si al mostrar una vista especificamos una variable de contexto que tenga exactamente el mismo nombre que una variable de contexto predeterminada el contenido de esta será reemplazado por el valor de la variable de contexto de la vista. Por ejemplo, definimos una variable de contexto predeterminada con el nombre title
la cual tiene un valor de Shopping Cart
, pero si al mostar una vista especificamos también una variable de contexto con el nombre title
el valor de la variable de contexto predetarminada será reemplazado por el valor de la variable de contexto de la vista.
Como esto puede prestarse a confusiones y posiblemente a comportamientos inesperados es recomendable nombrar las variables de contexto predeterminadas con un nombre no tan común, por ejemplo, iniciarlas con un guión bajo.
protected function render($view, $locals = [])
{
$context = [];
$context = array_merge((array)Yelu::$config->context, $locals);
$context['content'] = $this->internalRender($view, $locals);
echo $this->internalRender(Yelu::$config->layout, $context);
}
La función array_merge
toma dos arreglos, si algúna llave del primer arreglo existe en el segundo el valor retornado será reemplazado por el valor del segundo arreglo. Las llaves que no existan en el segundo arreglo serán retornadas como tal. Con esto podemos agregar tantas variables de contexto predeterminadas como necesitemos.
Agregando un estilo a la información
La información que estamos mostrando en el listado de productos hasta el momento no representa nada, por cada uno de los productos que tenemos en el arreglo vamos a crear un recuadro en donde mostraremos el nombre, descripción y precio del producto.
Hay que editar la vista items/list.php
y agregamos lo siguiente:
<div class="col-md-12">
<div class="page-header">
<h1>Productos</h1>
</div>
<div class="container">
<div class="row">
<?php
foreach ($items as $item) {
?>
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<div class="caption">
<h3><?= $item['producto'] ?></h3>
<p><?= $item['descripcion'] ?></p>
<p>
<span class="label label-primary">$<?= number_format($item['precio'], 0) ?></span>
</p>
</div>
</div>
</div>
<?php
}
?>
</div>
</div>
</div>
Recargamos el navegador y veremos que los productos aparecen en un recuadro con su información y el precio se encuentra dentro de un recuadro azul.
Paginación
El mostrar todos los productos en una sola pantalla es una mala idea, la información es bastante y no hay un orden. Vamos a realizar una paginación para ir mostrando de nueve productos por cada página.
Para comenzar, el calculo del rango de productos que vamos a mostrar se puede realizar de diferentes maneras y dependiendo del manejador de base de datos que estemos utilizando puede hacerse de una manera mas fácil que en otros.
Para este caso vamos a utilizar MySql y hacer uso de la clausula LIMIT
de la sentencia SELECT
la cual acepta dos argumentos, el primero que especifica el offset de la primera fila a retornar y el segundo el número máximo de filas retornar.
Nota: Podemos entender offset como el número de fila del cual comenzará a obtener registros.
Vamos a agregar un nuevo método público al modelo Item
de nombre getItemsPerPage
el cual va a solicitar dos parámetros de nombre $start
e $itemsPerPage
en el cual vamos a obtener la cantidad de productos que contenga el parámetro $itemsPerPage
de manera ordenada a partir del identificador $start
.
public function getItemsPerPage($start, $itemsPerPage)
{
$dbh = $this->getConnection();
$sth = $dbh->prepare('SELECT id, producto, descripcion, precio FROM items LIMIT :start, :itemsperpage;');
$sth->bindParam('start', $start, \PDO::PARAM_INT);
$sth->bindParam('itemsperpage', $itemsPerPage, \PDO::PARAM_INT);
$sth->execute();
return $sth->fetchAll();
}
Como ven, ordenamos los productos por el campo id
y obtenemos un rango de registros que inicia a partir del parámetro $start
y selecciona un total que define el parámetro $itemsPerPage
.
Hay que reemplazar el uso del método getItems
por getItemsPerPage
y colocaremos los valores 0
y 9
como valores de los parámetros que solicita el método para realizar una prueba.
El método
getItems
del modeloItem
podemos eliminarlo, ya no es necesario utilizarlo.
Si recargamos el navegador veremos que solo aparecen nueve productos y si realizamos cambios a los valores de los parámetros $start
e $itemsPerPage
veremos como la información cambia.
Vamos a calcular que productos mostrar dependiendo del número de página proporcionada, para ello necesitamos aplicar la siguiente regla:
El orden de los registros en MySql comienza en cero, podemos restar una unidad al número de página actual y multiplicar por la cantidad de registros a mostrar para obtener el número de registro inicial de la página, por ejemplo:
|Página|-1| |Registros por página|Registro inicial| |------|--|-|--------------------|----------------| |1 |0 |X|9 |0 | |2 |1 |X|9 |9 | |3 |2 |X|9 |18 | |4 |3 |X|9 |27 | |5 |4 |X|9 |36 |
Necesitaremos conocer que número de página vamos a mostrar, por lo tanto vamos a solicitar un parámetro de url de nombre page
, esto lo haremos en el método listPage
y si el parámetro no es enviado vamos a asignarle de manera predeterminada el valor 1, lo siguiente lo agregaremos al inicio del método.
$page = filter_input(INPUT_GET, 'page');
$page = $page != null ? $page : 1;
Ya teniendo el número de página vamos a aplicar la regla anterior definiendo el número de páginas a mostrar como nueve:
public function listPage()
{
$page = filter_input(INPUT_GET, 'page');
$page = $page != null ? $page : 1;
$itemModel = $this->getModel('Item');
$itemsPerPage = 9;
$start = ($page - 1) * $itemsPerPage;
$items = $itemModel->getItemsPerPage($start, $itemsPerPage);
$this->render('items/list', [
'items' => $items
]);
}
Hay que acceder a la ruta /items
pero ahora vamos a agregar el parámetro page
proporcionando un valor numérico como /items?page=1
. Veremos que aparece un mensaje como este La ruta GET /items?page=1 NO existe!
y es por que en realidad la configuración busca por una ruta de nombre /items?page=1
, vamos a corregir esto separando los parámetros GET de las rutas, cambiemos lo siguiente del constructor de la clase Router
:
$this->path = filter_input(INPUT_SERVER, 'REQUEST_URI');
por
$path = filter_input(INPUT_SERVER, 'REQUEST_URI');
$this->path = isset($path) ? explode('?', $path)[0] : '/';
para partir la url en dos en caso de encontrarse parámetros GET en la url y obtener solo el path, si no existe el path retornamos /
de manera predeterminada.
Recarguemos nuevamente el navegador para ver el listado de los productos y trate de cambiar el número de página para observar como el listado de productos cambia.
Hace falta agregar un método de navegación entre las páginas, por que el usuario no debería cambiar el número de página en la url manualmente para buscar mas productos, para ello necesitamos mostrar cuantas páginas existen por todos los productos. Esto puede ser en una barra inferior en la cual el usuario pueda navegar entre cada una de las páginas.
Para conocer el total de páginas primero deberíamos conocer el total de productos, hay que agregar un nuevo método al modelo Item
el cual haga lo que necesitamos, obtener el total de productos de la base de datos.
public function getTotal()
{
$dbh = $this->getConnection();
$sth = $dbh->query('SELECT COUNT(*) FROM items;');
return $sth->fetchColumn(0);
}
Hay que llamar al método desde el controlador Item
y calcular el número máximo de páginas dividiendo el total de productos entre los productos por pagina, despúes pasar el resultado de la operación a la vista.
public function listPage()
{
$page = filter_input(INPUT_GET, 'page');
$page = $page != null ? $page : 1;
$itemModel = $this->getModel('Item');
$totalItems = $itemModel->getTotalItems();
$itemsPerPage = 9;
$maxPages = ceil($totalItems / $itemsPerPage);
error_log("Número total de páginas: $maxPages");
$start = ($page - 1) * $itemsPerPage;
$items = $itemModel->getItemsPerPage($start, $itemsPerPage);
$this->render('items/list', [
'items' => $items,
'maxPages' => $maxPages
]);
}
Nota: utilizamos la función
ceil
para redondear el resultado fraccional de la divición hacia arriba.
Si imprimimos el valor de la variable $maxPages
en la vista items/list
veremos el número máximo de páginas a mostar en la terminal.
<?php echo "Número total de páginas: $maxPages"; ?>
Teniendo el número de páginas podemos crear la barra de navegación de la siguiente manera agregandolo despues del elemento div
que cuenta con la clase row
:
<div class="row">
<nav class="text-center">
<ul class="pagination">
<?php for ($i=1; $i <= $maxPages; $i++) { ?>
<li>
<a href="/items?page=<?= $i ?>"><?= $i ?>
</a>
</li>
<?php } ?>
</ul>
</nav>
</div>
También deberiamos resaltar el número de página activa, hay que agregar el número de página a las variables de contexto.
$this->render('items/list', [
'items' => $items,
'maxPages' => $maxPages,
'currentPage' => $page
]);
Por cada elemento li
validaremos si el número de página a imprimir es el actual, y si es asi, agregaremos la clase active
al elemento.
<div class="row">
<nav class="text-center">
<ul class="pagination">
<?php
for ($i=1; $i <= $maxPages; $i++) {
?>
<li<?= $i == $currentPage ? ' class="active"' : ''; ?>>
<a href="/items?page=<?= $i ?>"><?= $i ?>
</a>
</li>
<?php
}
?>
</ul>
</nav>
</div>
Una vista mas llamativa
Vamos a copiar las imágenes al directorio assets/images
y editaremos la vista items/items.php
para agregar la imagen correspondiente a cada producto.
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<div class="caption">
<img src="/assets/images/<?= $item['id'] ?>.jpg" alt="" />
<h3><?= $item['producto'] ?></h3>
<p><?= $item['descripcion'] ?></p>
<p>
<span class="label label-primary">$<?= number_format($item['precio'], 0) ?></span>
</p>
</div>
</div>
</div>
Además agregaremos lo siguiente al archivo CSS
assets/css/shoppingcart.css
para hacer que la imagen tenga un ancho fijo, el titulo tenga una altura fija y la descripción no sobrepase una altura que haga perder la alineación entre los productos.
.thumbnail .caption h3 {
min-height: 53px;
max-height: 53px;
}
.thumbnail .caption p.description {
position: relative;
height: 15.7em;
overflow: hidden;
}
.thumbnail .caption p.description:after {
content: "";
text-align: right;
position: absolute;
bottom: 0;
right: 0;
width: 70%;
height: 1.2em;
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 50%);
}
También vamos a editar la vista home.php
para agregar un enlace directo a la búsqueda de productos.
<div class="col-md-12">
<div class="jumbotron">
<h1>Bienvenido</h1>
<p>
Comienze a buscar productos ingresando a <a href="/items">nuestro extenso catálogo</a>.
</p>
</div>
</div>
Cuentas de usuario
Uno de los objetivos principales del proyecto es permitirle al usuario realizar compras, por lo cual es necesario que tengamos registrada la información del usuario que va a comprar. Para ello haremos un registro e inicio de sesión de usuario, donde solicitaremos un nombre de usuario y contraseña con los cuales identificaremos al usuario que se encuentra navegando en el sitio.
Hay que crear un controlador de autenticación de nombre Authentication.php
el cual contendrá una clase de nombre Authentication
y un método de nombre registerPage
en el cual vamos a cargar una vista de nombre auth/register
.
<?php
namespace Coppel\Controllers;
use Coppel\Yelu\BaseController;
class Authentication extends BaseController
{
public function __construct()
{
}
public function registerPage()
{
$this->render('auth/register');
}
}
El contenido de la vista es el siguiente:
<div class="col-md-2"></div>
<div class="col-md-6">
<container>
<row>
<h2 class="text-center">Registrar</h2>
<form class="form-horizontal" action="/auth/new" method="POST">
<div class="form-group">
<label class="col-sm-4 control-label">Usuario</label>
<div class="col-sm-8">
<input class="form-control" type="text" name="usuario" maxlength="32" />
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Nombre</label>
<div class="col-sm-8">
<input class="form-control" type="text" name="nombre" maxlength="60" />
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Contraseña</label>
<div class="col-sm-8">
<input class="form-control" type="password" name="password" maxlength="64" />
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Repetir contraseña</label>
<div class="col-sm-8">
<input class="form-control" type="password" name="password2" maxlength="64" />
</div>
</div>
<div class="form-group">
<div class="col-sm-4"></div>
<div class="col-sm-8">
<button class="btn btn-primary btn-block" type="submit">Registrar</button>
</div>
</div>
<div class="pull-right">
<a href="/auth/login">¿Iniciar sesión?</a>
</div>
</form>
</row>
</container>
</div>
<div class="col-md-4"></div>
Ahora, para poder mostrar esa vista es necesario configurar la ruta agregando lo siguiente:
[
"method" => "GET",
"path" => "/auth/register",
"handler" => [
"class" => "Authentication",
"method" => "registerPage"
]
]
Si ingresamos en el navegador la ruta /auth/register
podremos ver que para registrar un usuario estamos solicitando su nombre de usuario, nombre y contraseña, y si nos fijamos en el código fuente veremos que el formulario enviará los datos la ruta /auth/new
con el método POST
, vamos a crear esa ruta en la configuración y crearemos el método newAction
en el controlador Authentication
.
[
"method" => "POST",
"path" => "/auth/new",
"handler" => [
"class" => "Authentication",
"method" => "newAction"
]
]
En el método newAction
necesitamos obtener los datos de los campos del formulario y validar si fueron eviados.
public function newAction()
{
$usuario = filter_input(INPUT_POST, "usuario");
$nombre = filter_input(INPUT_POST, "nombre");
$password = filter_input(INPUT_POST, "password");
$password2 = filter_input(INPUT_POST, "password2");
if ($usuario != false && $nombre != false && $password != false && $password2 != false) {
echo "Todos los datos fueron enviados";
return;
}
echo "Faltan campos por enviar";
}
Sesiones y persistencia de datos
Si en la pantalla de registro enviamos el formulario sin llenar todos los campos deberiamos de ver el mensaje Faltan campos por enviar
en la pantalla, solo el mensaje sin ningúna vista, lo correcto seria redirigir a la ruta de registro y mostrar el mensaje. Para poder reelizar esto podemos agregar en el controlador base un método que nos sirva para redirigir a otra ruta como el siguiente:
protected function redirect($uri)
{
header("Location: $uri");
exit(0);
}
Entonces, en lugar de mostrar el mensaje hay que redirigir a la ruta /auth/register
:
$this->redirect('/auth/register');
Entonces, si enviamos el formulario con algún campo vacio el navegador deberia de redirigirnos hacia la pantalla de registro, pero hace falta mostrar un mensaje en el que mostremos que faltaron campos por enviar. Para poder realizar esto es necesario habilitar o iniciar el uso de sesiones en PHP.
Hay que editar el método start
de la clase Render
en el framework y agregar al inicio del método la función session_start()
. Con esto en cada petición iniciaremos el uso de las sesiones.
Con esto podemos utilizar la variable superglobal $_SESSION
para preservar la información entre peticiones.
Vamos a agregar el mensaje de validación cuando hagan falta datos por enviar y lo asignaremos al arreglo de sesión con el nombre message
y en el método registerPage
vamos a verificar si existe la llave message
en el arreglo de sesiones, de ser así hay que mostrarlo.
<?php
namespace Coppel\Controllers;
use Coppel\Yelu\BaseController;
class Authentication extends BaseController
{
public function __construct()
{
}
public function registerPage()
{
if (isset($_SESSION['message'])) {
$data = $_SESSION['message'];
echo $data;
}
$this->render('auth/register');
}
public function newAction()
{
$usuario = filter_input(INPUT_POST, "usuario");
$nombre = filter_input(INPUT_POST, "nombre");
$password = filter_input(INPUT_POST, "password");
$password2 = filter_input(INPUT_POST, "password2");
if ($usuario != false && $nombre != false && $password != false && $password2 != false) {
echo "Ok";
return;
}
$_SESSION['message'] = 'Es necesario especificar todos los campos';
$this->redirect('/auth/register');
}
}
Si realizamos la prueba nuevamente, enviar el formulario sin llenar todos los campos, veremos que el mensaje se muestra en la página, pero en el código fuente aparece fuera del contexto del código HTML, hay que pasarlo por las variables de contexto y mostrarlo en una sección en la página.
public function registerPage()
{
$locals = [];
if (isset($_SESSION['message'])) {
$locals['message'] = $_SESSION['message'];
}
$this->render('auth/register', $locals);
}
Dentro de la vista /auth/register
vamos a validar si la variable $message
existe y si es así mostraremos el mensaje en un recuadro de alerta. Lo siguiente hay que agregarlo despues de la rtiqueta de titulo h2
.
<?php
if (isset($message)) {
?>
<div class="alert alert-warning"><i class="glyphicon glyphicon-warning-sign"></i> <?= $message ?></div>
<?php
}
?>
Si nos fijamos bien, despues de enviar el formulario con datos faltantes, cada que ingresemos a la página de registro veremos el mensaje. El mensaje no desaparece por que el valor se mantiene hasta que lo eliminemos o lo desasignemos, o hasta que el servidor web sea reiniciado.
Para evitar lo anterior hay que desasignar el mensaje después de obtener su valor.
public function registerPage()
{
$locals = [
"title" => "Registro de nuevo usuario"
];
if (isset($_SESSION['message'])) {
$locals['message'] = $_SESSION['message'];
unset($_SESSION['message']);
}
$this->render('auth/register', $locals);
}
Ahora que ya podemos contar con información de manera persistente vamos agregar una validacion, hay que validar que las contraseñas que son enviadas coincidan, en caso contrario enviaremos un mensaje y redirigiremos a la página de registro.
if ($usuario != false && $nombre != false && $password != false && $password2 != false) {
if ($password != $password2) {
$_SESSION['message'] = "Las contraseñas no coinciden";
$this->redirect('/auth/register');
}
echo "Ok";
return;
}
Encriptando datos
Ya teniendo la información que necesitamos vamos a aplicar un método de encriptación a la contraseña proporcionada para no almacenarla tal cual es ingresada.
$hash = password_hash($password, PASSWORD_BCRYPT);
error_log("El password es $hash");
Veremos que la contraseña una vez encriptada tendrá una forma similiar a $2y$10$vXfTVJ.J3sY6noTQzRBTuekWjGQg6QJtY7f50rNpeTaP8b/MWMQzG
. Nada que ver con la contraseña proporcionada.
Modelo de usuario
Vamos a crear un nuevo modelo de nombre User
el cual se encargará de implemetar las acciones de registro e inicio de sesión de usuario.
<?php
namespace Coppel\Models;
use Coppel\Yelu\BaseModel;
class User extends BaseModel
{
public function __construct()
{
}
}
Y dentro de el crearemos un metodo en el cual vamos a registrar al usuario solicitando los datos de usuario para ser llamado desde del controlador Authentication
.
public function register($usuario, $hash, $nombre)
{
$dbh = $this->getConnection();
$sth = $dbh->prepare("INSERT INTO usuarios (usuario, nombre, hash) VALUES(:usuario, :nombre, :password)");
$sth->bindParam("usuario", $usuario, \PDO::PARAM_STR);
$sth->bindParam("nombre", $nombre, \PDO::PARAM_STR);
$sth->bindParam("password", $hash, \PDO::PARAM_STR);
$sth->execute();
return $dbh->lastInsertId();
}
public function newAction()
{
$usuario = filter_input(INPUT_POST, "usuario");
$nombre = filter_input(INPUT_POST, "nombre");
$password = filter_input(INPUT_POST, "password");
$password2 = filter_input(INPUT_POST, "password2");
if ($usuario != false && $nombre != false && $password != false && $password2 != false) {
if ($password != $password2) {
$_SESSION['message'] = "Las contraseñas no coinciden";
$this->redirect('/auth/register');
}
$hash = password_hash($password, PASSWORD_BCRYPT);
$userModel = $this->getModel('user');
$userModel->register($usuario, $hash, $nombre);
echo "Usuario registrado correctamente";
return;
}
$_SESSION['message'] = 'Es necesario especificar todos los campos';
$this->redirect('/auth/register');
}
Ahora vamos a crear un nuevo método publico en el controlador Authentication
de nombre loginPage
el cual mostrará una nueva vista de nombre auth/login
y deberá estár configurado con la ruta /auth/login
.
public function loginPage()
{
$this->render('auth/login');
}
public function loginPage()
{
$this->render('auth/login');
}
[
"method" => "GET",
"path" => "/auth/login",
"handler" => [
"class" => "Authentication",
"method" => "loginPage"
]
]
La vista tendrá lo siguiente:
<div class="col-sm-4">
</div>
<div class="col-sm-4">
<h2 class="text-center">Iniciar sesión</h2>
<?php
if (isset($message)) {
?>
<div class="alert alert-warning"><i class="glyphicon glyphicon-warning-sign"></i><?= $message?></div>;
<?php
}
?>
<form method="POST" action="/auth/authenticate" class="form-horizontal">
<div class="form-group">
<label class="control-label">Usuario</label>
<input class="form-control" type="text" name="username" />
</div>
<div class="form-group">
<label class="control-label">Contraseña:</label>
<input class="form-control" type="password" name="password" />
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" type="submit">Iniciar sesión</button>
</div>
<div class="pull-right">
<a href="/auth/register">¿Registrarse?</a>
</div>
</form>
</div>
<div class="col-sm-4">
</div>
Con esta vista el usuario podrá iniciar sesión y deberemos crear un nuevo método publico en el controlador Authentication
de nombre authenticateAction
para obtener los datos de inicio de sesión y validar que existan en la base de datos.
[
"method" => "POST",
"path" => "/auth/authenticate",
"handler" => [
"class" => "Authentication",
"method" => "authenticateAction"
]
]
En el modelo usuario agregaremos un nuevo método publico de nombre getDataByUsername
.
public function getDataByUsername($username)
{
$dbh = $this->getConnection();
$sth = $dbh->prepare("SELECT id, usuario, nombre, hash FROM usuarios WHERE usuario = :usuario");
$sth->bindParam("usuario", $username, PDO::PARAM_STR);
$sth->execute();
return $sth->fetch();
}
Implementando sesiones
PHP tiene soporte para el manejo y administración de sesiones la cual es una manera de preservar cierta información a través de accesos subsecuentes. Esto permite construir aplicaciones mas personalizadas e incrementar la experiencia de navegación.