Comunidad de diseño web y desarrollo en internet online

Implementar el patrón Modelo-Vista-Controlador en PHP

Hola a todos. Este es mi primer tutorial y trata acerca de cómo mejorar nuestras consultas a una base de datos utilizando PHP. Antes de continuar debo decir que hay un tip parecido que se llama Clase para conectar a una base de datos. El método que abordo es diferente al que proponen allí. No voy a profundizar en si alguno es mejor, creo que depende de las necesidades que se tengan en el momento.

Olvidaremos por un momento el código y planearemos que necesitamos hacer. ¿Cuales son las operaciones que hacemos más a menudo en una base de datos? seleccionar, insertar, actualizar y borrar. Mantendremos las cosas simples pero tendremos en mente dejar nuestra clase final lo suficientemente flexible por si alguien necesita extenderla para otras funciones (Alter table, etc...) .

¿Por qué necesitaríamos hacer más código si ya tenemos las funciones mysql_query y demás? Aquí entra el concepto de Modelo Vista-Controlador. Cuando ponemos funciones específicas de MySQL en nuestro script lo hacemos dependiente de un motor de BD. ¿Cómo nos va a ir si un día cambian a sqlite o algún otro? Ok eso no pasa todos los días pero aquí va una situación mas frecuente: ¿Te ha pasado que cuando le agregan un nuevo campo a la tabla o cambios similares deja de funcionar medio sistema? Tienes que recorrer todos los mysql_query y corregir las consultas. Dirá alguién 'pobre tonto que no usó SELECT * FROM tabla. Se usa mucho pero no es recomendable, el "*" hace que se analice la estructura de la tabla para saber qué campos tiene y eso afecta al rendimiento de la consulta.

Con esto ya tenemos nuestros requerimientos

  • Operaciones básicas.
  • Lista de campos.
  • No debe regresar un recurso MySQL, mejor algún tipo de matriz.

Tenemos una tabla usuarios que nos sirve de referencia, con estos campos:

  • id
  • nick
  • nombre
  • apellidos
  • mail
  • idciudad
  • idpais
  • fechaAlta

Y nuestra interfaz:

class UsuariosConnection
{
    public $table;
    public $fields;
    public function getRecords
    public function getRecord
    public function insertRecord
    public function updateRecord
    public function deleteRecord
}

¿Recuerdan el punto de que debe devolver una matriz y no un recurso? Haremos una función privada para eso:

private function sql ($consulta)
{
    $consQ =mysql_query (mysql_real_escape_string ($consulta));
    $resultado =array ();
    if ($consQ)
	 {
        while ($consF =mysql_fetch_assoc ($consQ))
        array_push ($resultado, $consF);
    }
    return $resultado;
}

La parte mysql_real_escape_string ayuda a evitar inyecciones de SQL. Esté código está programado para PHP5, pero es posible hacer la adaptación a PHP4, ya una vez lo tuve que hacer (en realidad no hay que cambiar tantas cosas).

Es turno de nuestras propiedades: $table y $fields. Esto irá dentro del constructor de la función:

public function __construct ()
{
    $this->table ='usuarios';
    $this->fields =array (
                          'id',
                          'nick',
                          'nombre',
            			   'apellidos',
            		       'mail',
            			   'idciudad',
            			   'idpais',
            			   'fechaAlta'
            			  );
}

La llamada a nuestra clase quedaría $myConnection =new UsuariosConnection ();.

Para getRecords necesitamos saber 4 cosas: condicion para WHERE, si se va a ordenar por algún campo, cuántos registros regresar, y desde qué indice comenzar (útil si estamos mostrando los resultados por páginas), pero también debemos permitir que se omitan.

public function getRecords ($where_str=false, $order_str=false, $count=false, $start=0)
{
    $where =$where_str ? "WHERE $where_str" : "";
    $order =$order_str ? "ORDER BY $order_str ASC" : "";
    $count = $count ? "LIMIT $start, $count" : "";
    $campos =implode (', ', $this->fields);
    $query ="SELECT $campos FROM {$this->table} $where $order $limit";
    return $this->sql ($query);
}        

Por cada parámetro, si se ha definido construimos el fragmento de SQL que corresponde, si no dejamos el espacio en blanco. ¿Para qué queremos una función getRecord si en getRecords (con S final) tenemos oportunidad de poner una condicion? Comodidad.

public function getRecord ($id)
{
    return $this->getRecords ("id=$id", false, 1);
}    	

getRecord sólo nos pide el id. Al poner el 3er parámetro en 1 (verás que inserta un LIMIT 1, 0) hacemos que el motor se detenga en cuanto encuentre el primer resultado. En ciertos casos se recorre la tabla buscando más registros (lo que nos trae de vuelta al tema del rendimiento). Si sabemos que sólo va a haber un registro, qué nos cuesta decírselo a la BD.

La consulta nos regresará el id de la cuidad y el país. Si queremos saber cómo se llama la cuidad y el país (están listados en otras tablas), debemos hacer otras dos consultas. ¿y que tal si los regresamos dentro de ésta misma? Eso es sencillo.

public function __construct ()
{
    $this->table ='usuarios';
    $this->fields =array (
            				'id',
            				'nick',
            				'nombre',
            				'apellidos',
            				'mail',
            				'idciudad',
            				'idpais',
            				'fechaAlta',
            				'(SELECT nombre FROM ciudades WHERE idciudad=ciudades.id) AS cuidad',
            				'(SELECT nombre FROM paises WHERE idpais=paises.id) AS pais'
           			   );
}

Y con eso, además del id de la ciudad y el país, tenemos el nombre. Y nos ahorramos dos consultas.

Ahora insertRecord. Recibiremos un arreglo. Los valores de cada campo deben estar en el orden en que estan declarados en $this->fields aunque con adaptaciones podemos recibir un arreglo asociativo y ya no hay problema con el orden que tengan. Las subconsultas que nos ahorraban 2 consultas ahora nos causarán problemas acá. ¿Hay que quitarlas? Nos pasamos a updateRecord para ver el alcance del problema.

public function updateRecord ($id, $data)
{
    $campos =$this->fields;
    $datos =array ();
    foreach ($campos as $ind => $campo)
    {
        $current_data =$data[$ind];
        array_push ($datos, "$campo='$current_data'");
    }
    $datos =implode (", ", $datos);
    mysql_query ("UPDATE {$this->table} SET $datos WHERE id=$id");
}

Ok, quitamos las subconsultas pero hay un punto débil en nuestra clase que puede ser peligroso: Los campos id y fechaAlta se pueden modificar.

Clasificaremos nuestros campos en tres tipos y nuestro arreglo $fields se vuelve una matriz asociativa.

$this->fields =array (
        				array ('private',    'id'"''"),
        				array ('public',    'nick'),
        				array ('public',    'nombre'),
        				array ('public',    'apellidos'),
        				array ('public',    'mail'),
        				array ('public',    'idciudad'),
        				array ('public',    'idpais'),
        				array ('private',    'fechaAlta',  'now()'),
        				array ('system',    '(SELECT nombre FROM ciudades WHERE idciudad=ciudades.id) AS cuidad'),
        				array ('system',    '(SELECT nombre FROM paises WHERE idpais=paises.id) AS pais')
   				  );    

Omitiendo el proceso de análisis, los campos privados tienen un valor que se va a usar por default en las consultas de inserción (algo como INSERT INTO usuarios (id, fechaAlta) VALUES ('', now());

Debemos hacer funciones para manejar los tipos de campos y los valores por defecto, además de reescribir parte de lo ya hecho. Ya no me detengo en esto. Pongo el código completo al final del tutorial.

¿Podríamos saber si por lo menos la consulta corrió bien?

Lo comprobamos asi:

private function validateOperation ()
{
    return mysql_error()=='' ? true : false;
}

Y como última instrucción de un insertRecord, updateRecord, deleteRecord escribimos return $this->validateOperation ();

¿Por cada tabla debemos copiar y pegar todo el código y cambiar las propiedades $table y $fields? Podemos hacerlo mejor. Que ésta sea una clase abstracta (El funcionamiento es muy general, no especifica una tabla en particular sino que sirve de base para otras clase que sí lo harán).

public function __construct ($table)
{
    $this->table =$table;
    $this->fields =array ();
}

Y nuestra clase en adelante se llama SQLConection.

Para usarla directamente:

$tablaUsuarios =new SQLConection ('usuarios');
$tablaUsuarios->fields =array (/* los campos como ya los habíamos declarado antes */);  

Y si queremos extender la clase:

<?php
    require_once ("sqlconfig.php");
    /*
    Contenido de sqlconfig.php:
        <?php
        mysql_pconnect ("localhost", "root", "");
        mysql_select_db ("pruebas");
        ?>
    */
        require_once ("SQLConection.class.php");
        /** Clase para acceso de datos
        * Hereda de: SQLConection
        * @version: 1.0
        * @author: Arturo Leonardo Molina Aguilar (lama_amx AT hotmail DOT com)
        */
        class Usuarios extends SQLConection
		 {
            public function __construct ()
			 {
                parent::__construct ("usuarios");
                $this->fields = array (
                    			array ('private',    'id'"''"),
                   			array ('public',    'nick'),
                   			array ('public',    'nombre'),
                   			array ('public',    'apellidos'),
                    			array ('public',    'mail'),
                    			array ('public',    'idciudad'),
                   			array ('public',    'idpais'),
                   			array ('private',    'fechaAlta',  'now()'),
                    			array ('system',    '(SELECT nombre FROM ciudades WHERE idciudad=ciudades.id) AS cuidad'),
                    			array ('system',    '(SELECT nombre FROM paises WHERE idpais=paises.id) AS pais')
                    			);
             }
        }
    ?>
		

¿Ventajas de separar el manejo de datos del resto del código? Si hace falta usar otro motor de BD sólo se hace otra clase que mantenga los mismos nombres de métodos, solo cambiando su funcionamiento interno por el de la BD a usar.

Usando esto en Flex/WebORB me di cuenta de que es preferible devolver el recursos MySQL en getRecords. Solución: una propiedad privada que indica si se regresa matriz o el recurso.

Espero que les sea de utilidad.

¿Sabes SQL? ¿No-SQL? Aprende MySQL, PostgreSQL, MongoDB, Redis y más con el Curso Profesional de Bases de Datos que empieza el martes, en vivo.

Descargar Archivo

Publica tu comentario

El autor de este artículo ha cerrado los comentarios. Si tienes preguntas o comentarios, puedes hacerlos en el foro

Entra al foro y participa en la discusión

o puedes...

¿Estás registrado en Cristalab y quieres
publicar tu URL y avatar?

¿No estás registrado aún pero quieres hacerlo antes de publicar tu comentario?

Registrate