Métodos Mágicos FindBy

Muchos de los mapeadores de objetos relacionales o ORM, implementan los llamados métodos mágicos findBy que permiten la búsqueda de registros por cualquier campo de la tabla

07-Dic-2017

Métodos Mágicos

La mayoría de los frameworks existentes para el desarrollo de aplicaciones en PHP, disponen de componentes que nos permiten abstraernos de la base de datos a la hora de crear consultas que incluyen multitud de métodos para la selección de datos. Entre estos métodos es frecuente encontrar los llamados métodos mágicos findBy, como es el caso del framework Symfony que a través del componente Doctrine nos permite la selección de datos utilizando el método findbyNombreCampo.

Los métodos findBy añaden el nombre del campo para el que deseamos realizar la consulta, simplificando notablemente la selección de registros cuando la condición viene aplicada a un solo campo.

Si deseáramos buscar por el campo id el método se llamaría findById, si el campo se llamara provincia el método se llamaría findByProvincia, si el campo se llamara teléfono el método se llamaría findByTelefono y así para el resto de los campos.

Supongamos que deseamos seleccionar un usuario por medio del campo Email cuando este fuese ‘pepe@servidor.com’, sólo tendríamos que llamar al método findByEmail(‘pepe@servidor.com’) para obtener el registro del usuario. Observar que lo que hemos hecho es añadir el nombre del campo por el que deseamos realizar la consulta al método findBy ‘findByEmail’, pasándole como argumento el email del usuario.

Método mágico de PHP __call

Si quisiéramos implementar esta funcionalidad por medio de la definición de métodos con los nombres de los campos, resultaría una tarea absurda, nada práctica y muy laboriosa, ya que necesitaríamos un método por cada campo de nuestras tablas sin la posibilidad de repetir sus nombres.

Entonces ¿cómo lo vamos a conseguir? Utilizando el método __call.

PHP nos ofrece este método mágico __call que es ejecutado cuando se intenta acceder a un método no definido en el contexto del objeto.

public mixed __call ( string  $name , array $arguments ) 

El parámetro $name representa el nombre del método que se intentaba ejecutar y $arguments es un array que contiene los parámetros que se le estaban pasando al método.

Esto significa que cuando intentemos ejecutar un método de la clase no definido, PHP lanzará el método __call, donde podremos conocer el nombre del método que se intentaba ejecutar y los parámetros que se le enviaban, justo lo que ocurrirá cuando intentemos ejecutar nuestros métodos findBy.

Implementarlo en nuestra clase principal

Una vez que sabemos que método utilizar para poder generar nuestros métodos mágicos findBy, añadiremos el código a nuestra clase CnnMySQL y de esta manera aumentar la funcionalidad de nuestra capa de datos.

Cuando se ejecute el método __call, comprobaremos si el parámetro $name que corresponde con el método que se estaba intentando ejecutar, comienza por ‘findBy’. Si se cumple esta comprobación el resto de la cadena corresponderá con el nombre del campo sobre el que tenemos que realizar la consulta. De lo contrario generaremos un mensaje de error deteniendo la ejecución del código.

Una vez que conocemos el nombre del campo, comprobaremos si la tabla sobre la que hay que ejecutar la consulta tiene un campo con ese nombre, y si existe crearemos la condición necesaria y ejecutaremos la consulta, de lo contrario generaremos un mensaje de error deteniendo la ejecución del código.

Para realizar la comprobación del nombre del campo, voy a crear un par de métodos adicionales: uno que obtiene todos los campos de una tabla ‘camposTabla’ y otro que nos indica si existe un campo en una tabla ‘extCampoTabla’.

Teniendo en cuenta las instrucciones comentadas anteriormente, el método __call quedaría de la siguiente manera:

<?php
// Recibe como parámetro el nombre del método inexistente en nuestra clase y sus parámetros
public function __call($name, $arguments) {

// Si el nombre del método comienza por la cadena findBy
  if(substr($name,0,6)==='findBy') {
    // Extraemos el nombre del campo
    $Campo= strtolower(substr($name,6));
// Si no era un método mágico findBy, mostramos mensaje y detenemos el código 
  } else {
    die("Procedimiento $name inexistente en la clase ".__CLASS__);
  }

// Si el campo existe en la tabla activa
  if($this->extCampoTabla($Campo)) {
    // Creamos la cláusula WHERE con el valor del primer parámetro
    $where="$Campo=".$this->GetValor($arguments[0]);
    // Preparamos los parámetros de la consulta SELECT
    $args['Where']=$where;
    // Devolvemos el resultado de ejecutar la consulta SELECT
    return $this->select($args);   
// Si la tabla no tiene ese campo, mostramos mensaje y detenemos el código 
  } else {
    die('El campo '.$Campo.' no forma parte de la tabla '.$this->TablaBD);
  }

}

// Recibe como parámetro el nombre de la tabla y devuelve un array con los nombres de sus campos
protected function camposTabla($tabla) {
    // Consulta para obtener información de los campos de la tabla
    $ColSQL="SHOW COLUMNS FROM ". $tabla;
    // Obtenemos un array con el resultado de la consulta
    $columnas= $this->ArraySelect($ColSQL);
    $campos=[];
    // Recorremos el resultado de la consulta y nos vamos guardando el nombre del campo
    foreach ($columnas as $campoTabla) {
        $campos[]=$campoTabla['Field'];
    }
    // Devolvemos el array con los nombres de los campos
    return $campos;    
}

// Recibe como parámetro el nombre del campo y devuelve un valor booleano que nos indica si la tabla activa tiene un campo con ese nombre
private function extCampoTabla($Campo) {
    // Obtenemos el array con los nombres de los campos de la tabla activa
    $campos=$this->camposTabla($this->TablaBD);
    // Comprobamos si el nombre del campo se encuentra en la lista de campos que hemos obtenido
    $existeCampo=in_array($Campo,$campos);
    // Devolvemos el resultado de la comprobación
    return $existeCampo;
}

NOTA: debido a que el resultado obtenido será un array con los registros seleccionados, si deseamos extraer directamente el registro cuando sepamos que sólo se va a obtener uno, añadiremos [0]  a continuación de la llamada al método mágico findBy. Para entender mejor este comentario en el siguiente apartado se muestran ejemplos de su utilización.

Aplicar el nuevo método a las subclases de la capa de datos

Una vez creado nuestro método mágico, todas las subclases de la capa de datos podrán utilizarlo cuando sea necesario ya que todas heredan de la clase principal CnnMySQL.

Veamos algunos ejemplos:

Deseamos obtener el registro del usuario con email = ‘novice@gmail.com’ y el registro del usuario con teléfono=’666596844’

<?php
require_once($_SERVER['DOCUMENT_ROOT']. 'autocarga.php');

$Usuarios = new TbUsuarios();

$email='novice@gmail.com';
$RegUsuario=$Usuarios->findByEmail($email)[0]; // Obtener directamente el primer registro de los resultados
$nick='Camaleón';
$RegUsuario=$Usuarios->findByNick($nick)[0]; // Ídem que para el caso del campo email

Si el método lo aplicamos sobre algún campo que repita valores, como puede ser el caso del campo categoría de la tabla tbentradas, obtendremos grupos de registros en lugar de un único registro.

<?php
require_once($_SERVER['DOCUMENT_ROOT']. 'autocarga.php');

$Entradas = new TbEntradas();

$categoria='Programación';
$Registros=$Entradas->findByCategoria($categoria);
// En este caso no hemos añadido [0] a continuación del método ya que se supone que podemos obtener muchos registros

foreach($Registros as $Registro) {
  echo '<div class="panel panel-info text-justify">' .
       '<div class="panel-heading">' .
       '<h2 class="panel-title">'.$Registro['titulo'].'</h2>' .
       '</div>' .
       '<div class="panel-body">' .
       '<p>Publicado: '.date("d-m-Y",strtotime($Registro['fecha'])).'</p>' .
       '<div>'.$Registro['contenido'].'</div>' .
       '</div>' .
       '</div>';
}

Mostrar todos los comentarios realizados por el usuario con Nick=’camaleon’

<?php
require_once($_SERVER['DOCUMENT_ROOT']. 'autocarga.php');

$Usuarios = new TbUsuarios();
$Comentarios = new TbComentarios();

$nick='camaleon';
$Usuario=$Usuarios->findByNick($nick)[0];
$Registros=$Comentarios->findById_Usuario($Usuario['id']);

foreach($Registros as $Registro) {
  echo '<div class="panel panel-info text-justify">' .
       '<div class="panel-heading">' .
       '<h2 class="panel-title">'.$Registro['titulo'].'</h2>' .
       '</div>' .
       '<div class="panel-body">' .
       '<p>Publicado: '.date("d-m-Y",strtotime($Registro['fecha'])).'</p>' .
       '<div>'.$Registro['contenido'].'</div>' .
       '</div>' .
       '</div>';
}

Crear nuevos métodos mágicos

A partir de las indicaciones dadas en los apartados anteriores, podríamos implementar nuevos métodos mágicos, sabiendo que el truco principal consiste en comprobar si el nombre del método comienza por un patrón preestablecido, como en nuestro caso era la cadena ‘findBy’.

De esta manera podríamos definir el método deleteBy que nos permitiera eliminar los registros de una tabla a partir de la condición de un campo.

Por ejemplo, si llamáramos al método deleteByEmail(‘nuevo@dominio.com’), eliminaríamos el registro del usuario cuyo email fuera ‘nuevo@dominio.com.

La implementación sería idéntica a la del método findBy, donde lo único que cambiaría sería que en lugar de ejecutar el método select(), ejecutaríamos el método delete().

Ejemplos uso de las subclases

Junto a la documentación de este módulo se entrega un archivo comprimido con:

  • Base de datos blog (en formato sql para importarla a nuestro servidor local MySQL)
  • Script para crear el usuario con permisos de acceso a la base de datos en formato .sql.
  • Carpeta clases con la clase principal CnnMySQL actualizada, las subclases y el archivo de configuraciones.
  • Script de autocarga de clases.
  • Carpeta ejemplos con los ejemplos comentados en este módulo y en los módulos anteriores.