2013-04-23

Gestión de datos en PHP

Os voy a exponer como hago yo la gestión de datos en PHP, apoyándome en el modelado con clases y el patrón DAO (Data Access Object).

Los Objetos de Acceso a Datos son un patrón de diseño J2EE considerados una buena práctica. Un DAO permite abstraer y encapsular todos los accesos a la fuente de datos (normalmente una base de datos, como MySQL). EL DAO gestiona la conexión con la fuente de datos para obtener y almacenar datos, implementando los mecanismos de acceso requeridos.  El DAO oculta los detalles de implementación del origen de datos a sus clientes. Debido a que la interfaz expuesta por el DAO a los clientes no cambia cuando cambia la implementación del origen de datos suybacente, este modelo permite al DAO adaptarse a diferentes esquemas de almacenamiento sin afectar a sus clientes o componentes de negocio.

El siguiente diagrama de clases representa las relaciones del patrón DAO:
Y en el siguiente diagrama de secuencia se representa la interacción entre los distintos participantes del patrón:
El BusinessObject es el cliente que requiere el acceso a la fuente de datos para obtener y almacenar datos. El DataAccessObject es en quien delega el BusinessObject para hacer estas operaciones. El DataSource representa una implementación de una fuente de datos (como un RDBMS). El TransferObject representa un soporte temporal de datos. El DataAccessObject puede utilizar un TransferObject para devolver datos al cliente o recibir los datos del cliente en un TransferObject para actualizar los datos del DataSource.


Esta es la manera en que yo aplico el patrón DAO en PHP para gestionar los datos en las aplicaciones de forma uniforme, encapsulada y sencilla:

Voy a utilizar como ejemplo la siguiente tabla FABRICANTES:
(Ver: Modelo de datos)
CREATE TABLE fabricantes (
  id smallint(5) NOT NULL AUTO_INCREMENT,
  codigo varchar(10) NOT NULL,
  denominacion_social varchar(100) NOT NULL,
  activo tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
  1. Por cada tabla creamos dos clases PHP:
    (Para no tener que hacer includes de las clasese en los scripts, ver: )
    • class.fabricante.php, que será el TransferObject, con variables para cada campo de la tabla:
    <?php
    class Fabricante extends Desactivable {
     
    public $id;
    public $codigo;
    public $denominacion_social;
    
    }
    ?>
    
    • class.fabricantes.php, que será el DataAccessObject
    <?php
    define('FABRICANTES_FIELDS_SIN_ID', 'codigo, denominacion_social');
    define('FABRICANTES_FIELDS', 'id, ' . FABRICANTES_FIELDS_SIN_ID);
    define('FABRICANTES_SELECT', 'SELECT ' . FABRICANTES_FIELDS . ' FROM fabricantes WHERE 1 = 1');
    define('FABRICANTES_ORDER', ' ORDER BY denominacion_social');
    define('FABRICANTES_INSERT', 'INSERT INTO fabricantes (' . FABRICANTES_FIELDS_SIN_ID . ') VALUES (%s, %s)');
    define('FABRICANTES_UPDATE', 'UPDATE fabricantes SET codigo = %s, denominacion_social = %s WHERE id = %s');
    define('FABRICANTES_DELETE', 'UPDATE fabricantes SET activo = 0 WHERE activo = 1');
    
    class Fabricantes {
    
     function getFabricante($id, $activo = true) {
      $sql = sprintf(FABRICANTES_SELECT . ($activo? ' AND activo = 1' : '') . ' AND id = %s',
        param($id));
      $result = mysql_query($sql) or trigger_error(mysql_error());
      if ($obj = mysql_fetch_object($result, 'Fabricante')) {
       return $obj;
      }
    
      return false;
     }
    
     function getFabricantes($fab = null, $orden = null, $ordenado = null) {
      $sql = FABRICANTES_SELECT . ' AND activo = 1';
      if (isset($fab)) {
       if ($fab->codigo != '') {
        $sql .= sprintf(' AND codigo = %s',
          param($fab->codigo));
       }
      }
      if (isset($orden)) {
       $sql .= ' ORDER BY ' . $orden . '';
       if (isset($ordenado)) {
        $sql .= ' ' . $ordenado;
       }
      } else {
       $sql .= FABRICANTES_ORDER;
      }
      $result = mysql_query($sql) or trigger_error(mysql_error());
      while ($obj = mysql_fetch_object($result, 'Fabricante')) {
       $objs[] = $obj;
      }
     
      if (isset($objs)) {
       return $objs;
      }
      return false;
     } 
    
     function setFabricante($fab) {
      if ($fab->id == null) {
       $sql = sprintf(FABRICANTES_INSERT,
         param($fab->codigo), param($fab->denominacion_social));
      } else {
       $sql = sprintf(FABRICANTES_UPDATE,
         param($fab->codigo), param($fab->denominacion_social), $fab->id);
      }
      mysql_query($sql) or trigger_error(mysql_error());
      if (mysql_affected_rows() === 1) {
       if ($fab->id == null) {
        $fab->id = mysql_insert_id();
       }
       return true;
      }
     
      return false;
     } 
    
     function remFabricantes($ids) {
      $sql = sprintf(FABRICANTES_DELETE . ' AND id IN (%s)', $ids);
      mysql_query($sql) or trigger_error(mysql_error());
      return mysql_affected_rows();
     } 
     
    }
    ?>
    
  2. Uso desde un cliente o BusinessObject:
    (Ver Autocarga de clases en PHP)
    <?php
    $fabMgr = new Fabricantes();
    if ($_POST['id'] != '') {
     $fab = $fabMgr->getFabricante($_POST['id']);
    } else {
     $fab = new Fabricante();
    }
    
    $fab->codigo = $_POST['codigo'];
    $fab->denominacion_social = $_POST['denominacion_social'];
     
    if ($fabMgr->setFabricante($fab)) { 
     $cambios = $fab->id;
    } else {
     $errores = 'Fabricante no guardado';
    }
    ?>
    

2013-03-03

Autocarga de clases en PHP

Cuando creamos un fichero fuente PHP para cada definición de clase y queremos evitar tener que incluir al comienzo de cada script todos los includes necesarios de las clases que usan, se puede definir una función __autoload(), que es automáticamente invocada en caso de que se esté intentando utilizar una clase que no ha sido definida. Esta función podríamos meterla en un fichero que se incluya siempre al principio de cada script y tendría, por ejemplo, esta definición:
<?php
function __autoload($nombre_clase) {
 include 'dao/class.' . $nombre_clase . '.php';
}
?>
Entonces, en lugar de tener un script así:
<?php
require('dao/class.cliente.php');
require('dao/class.clientes.php');
require('dao/class.pedido.php');
require('dao/class.pedidos.php');
require('dao/class.producto.php');
require('dao/class.productos.php');
require('dao/class.almacen.php');
require('dao/class.almacenes.php');

$cliente = new Cliente();

$pedido = new Pedido($cliente);

...
?>
Podemos tener:
<?php
require('base/start.php');

$cliente = new Cliente();

$pedido = new Pedido($cliente);

...
?>

NOTA: A partir de PHP 5.1.2 se recomienda el uso de spl_autoload_register() como alternativa a __autoload() [php.net]

2013-02-01

Cómo configurar un directorio virtual en Apache

Mediante directorios virtuales podemos acceder a los recursos que están fuera del directorio raíz del servidor (DocumentRoot), por ejemplo en C:\ruta\a\directorio mediante una url como http://localhost/mialias

Yo utilizo esto, por ejemplo, para tener la versión en desarrollo de los proyectos en una ruta distinta a la de la instalación del servidor pero poder probarla directamente en el servidor.

Para configurar un directorio virtual en Apache tenemos que:
  1. Abrir el fichero httpd.conf
  2. Buscar el bloque
    <IfModule alias_module>
    
  3. Y después de
    ScriptAlias /cgi-bin/ "C:/xampp/cgi-bin/"
    
  4. Añadir la siguiente línea:
    Alias "/mialias" "C:\ruta\a\directorio"
    
  5. Por último, después de
    <Directory "C:/xampp/cgi-bin">
     AllowOverride None
     Options None
     Require all granted
    </Directory>
    
  6. Añadir el siguiente bloque:
    <Directory "C:\ruta\a\directorio">
     Options Indexes FollowSymLinks Includes ExecCGI
     AllowOverride All
     Order allow,deny
     Allow from all
     Require all granted
    </Directory>
    
  7. Reiniciando Apache ya podremos navegar a http://localhost/mialias

2013-01-24

Clases para un modelo de datos en PHP

Para los modelos de datos que presenté (Modelo de datos) presento el conjunto de clases que lo soportan haciendo uso de la herencia.

Primero un diagrama:

// TODO

Y ahora el código de las clases, junto con las 2 clases de ejemplo, MiClaseDesactivable y MiClaseHistoriable:

<?php
abstract class Desactivable {
 
 public $activo;
 public $alta;
 public $baja;
 
}
?>
<?php
abstract class Historiable extends Desactivable {
 
 public $inicio;
 public $fin;
 
}
?>
Como me gusta hacer borrados lógicos en vez de físicos en las tablas de datos, todas las clases de mis proyectos heredarán de una de éstas 2 clases abstractas, directamente de Desactivable, para los casos generales, o de Historiable, para tablas versionadas o históricas (Slowly Changing Dimension de tipo 2). Ver http://raulcad.blogspot.com.es/2013/01/modelo-de-datos.html
<?php
class MiClaseDesactivable extends Desactivable {
 
 // TODO añadir los campos propios
 
}
?>
<?php
class MiClaseHistoriable extends Historiable {
 
 // TODO añadir los campos propios
 
}
?>

2013-01-14

Modelo de datos

Voy a exponer un modelo de datos general para el diseño de bases de datos que a mi me resulta práctico a la hora de trabajar:

Estos serían los campos básicos de una tabla:
  • id smallint(5) NOT NULL AUTO_INCREMENT
    Tener un identificador único para cada fila (se denomina Surrogate Key) tarde o temprano se agradece mucho más que tener que identificarlos mediante claves compuestas por varios campos. Además, como me gusta utilizar tablas versionadas o históricas (Slowly Changing Dimension de tipo 2), las Claves de Negocio (Business Key) pueden no tener un valor único en la tabla. Este concepto es muy utilizado en el diseño de bases de datos en entornos de Data Warehouse y Business Intelligence. En una tabla versionada SCD Tipo 2 es posible realizar un seguimiento de los cambios almacenando en cada fila, es decir, en cada versión, el periodo de tiempo para el cuál es válida la información de dicha fila (por ejemplo, incluyendo dos campos Fecha Desde y Fecha Hasta en la tabla). En consecuencia, ya no podremos definir la Clave de Negocio como un campo único, ya que pueden existir múltiples filas (múltiples versiones, tanto la actual como una o varias versiones históricas) con el mismo valor de la Clave de Negocio (Business Key). El campo id almacena un valor numérico único para cada fila de la tabla, actuando como una clave sustituta, de forma totalmente independiente a los datos de negocio. [guillesql.es]
  • activo tinyint(1) NOT NULL DEFAULT '1'
    Permite realizar borrados lógicos ("UPDATE tabla SET activo = 0" en vez de "DELETE FROM tabla") de los datos de la tabla y de esta manera es fácil restaurar datos y mantener el histórico de los datos en la tabla.
Los siguientes campos son la fecha de inserción de la fila y del borrado lógico de la misma, para poder hacer un mejor seguimiento de las acciones. No me gusta incluir otros campos de auditoría como pueden ser el usuario de las acciones o fechas de modificación. Si estos datos son relevantes, prefiero guardarlos en otra tabla a modo de log.
  • alta datetime NOT NULL
  • baja datetime DEFAULT NULL
Y por último, los dos campos siguientes los incluyo cuando necesito que haya más de un registro de una determinada información, cada uno de los cuales tiene la información válida correspondiente a un cierto periodo de tiempo:
  • inicio datetime NOT NULL
  • fin datetime DEFAULT NULL
Por ejemplo, una tabla de impuestos en la que queremos poder consultar el porcentaje de IVA general que era válido en 2011 y el que está vigente ahora, según mi modelo tendría esta forma:
id activo alta baja inicio fin porcentaje_iva
1
1
15/06/2010 08:09:10
01/07/2010 00:00:00
31/08/2012 23:59:59
18
2
1
20/08/2012 11:10:09
01/09/2012 00:00:00
21