martes, 16 de julio de 2013

PDO - Parte 1

La extensión PDO nos facilitará mucho la vida a la hora de trabajar con bases de datos, ya que nos permite abstraernos del tipo de gestor de base de datos que estemos utilizando. Esto quiere decir que sin el uso de PDP, si por ejemplo decides migrar tu aplicación de MySQL a PostgreSQL tendrías que cambiar todos los métodos del conector de MySQL por los métodos de PostgreSQL. Con PDO bastaría con que se cambie únicamente la creación del objeto PDO, donde se especifica el tipo de base de datos a conectar, y el resto de la aplicación seguiría funcionando igual. Por lo esta abstracción del acceso de datos convierte a esta extensión en un elemento muy importante para la portabilidad de la aplicación/web.
PDO además te permitirá usar consultas preparadas. Y como vimos en el caso de la extensión MySQLi nos proporciona una herramienta muy importante en seguridad y rendimiento.
- Se encargan de 'desinfectar' los datos automáticamente.
- La separación de lógica y datos hace que la consulta sea analizada/preparada una única vez y se pueda ejecutar múltiples veces con los mismos datos o similares. Evitando cada vez el ciclo de análisis, compilación y optimización que ocurriría en cada consulta no preparada.
- Al igual que Mysqli, proporciona el uso de transacciones.

Pasos

1) Conexión con la base de datos

Lo primero que tenemos que hacer es realizar la conexión a la base de datos. La conexión la realizaremos con el constructor de la clase de la siguiente forma:

 $con = new PDO('mysql:dbname=testdb;host=127.0.0.1', $user, $password);  

Como se puede apreciar, en el primer parámetro se indicará el tipo de gestor de base de datos al que se conectará PDO, seguido del nombre de la base de datos y del host donde estará dicha base de datos. A este parámetro se le llama DSN.
El usuario y la contraseña son parámetros opcionales y dependen de si la base de datos los requiere.

2) Especificar el tratamiento de errores

Por defecto PDO viene configurado para que no se muestre ningún error. Por lo que tras cada consulta (incluida la conexión) tendríamos que hacer uso de los métodos errorCode() y errorInfo().

 //errorInfo devuelve un array con información del error   
 print_r($con->errorInfo());  

Pero para ayudarnos, PDO permite activar el uso de excepciones. De tal manera que hay algún error tanto en conexión como en una consulta, saltará una excepción que podremos capturar. Para ello utilizaremos el método setAttribute y el atributo PDO::ATTR_ERRMODE. Acompañando a dicho atributo indicaremos uno de los 3 siguientes posibles valores:
- PDO::ERRMODE_SILENT es el valor por defecto. Con dicho valor no se lanzará ningún error ni excepción. Y como se ha comentado, el programador es el encargado de comprobar mediante  errorCode() y errorInfo() los posibles errores.
- PDO::ERRMODE_EXCEPTION es el valor que habilitará que se lancen excepciones. Es el valor que nos interesa.
- PDO::ERRMODE_WARNING genera un error E_WARNING de PHP si ocurre algún error.

 $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);  

Por lo tanto vamos a reescribir todo lo anterior adaptado al uso de excepciones

 try  
 {  
      $con = new PDO('mysql:dbname=testdb;host=127.0.0.1', $user, $password);  
      $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);  
 }  
 catch(PDOException $e)  
 {  
      echo 'Error: ' . $e->getMessage();  
 }  

PDO proporciona dos métodos para crear consultas:

3) Consultas no preparadas

Con el método query() tenemos la alternativa al uso de consultas preparadas. Utilizado mayormente cuando la consulta a realizar no contiene parámetros externos. Ya que este método no escapa automáticamente los parámetros. Además no nos beneficiaremos de la mejora de rendimiento de consultas múltiples.
Este método ejecuta la consulta (no necesita preparación) y devuelve un objeto PDOStatement en caso de acierto y False en caso de error. Como veremos posteriormente, las consultas preparadas necesitan 2 pasos antes de poder recoger el resultado: preparar (prepare()) y ejecutar (execute).

 try  
 {  
      $con = new PDO('mysql:dbname=demo';host=localhost', $user, $password);  
      $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);  
      $stmt = $con->query('SELECT nombre FROM usuarios');   
 }  
 catch(PDOException $e)  
 {  
      echo 'Error: ' . $e->getMessage();  
 }  

Como se ha comentado, este método no es aconsejable para sentencias con parámetros externos. Pero si aun así se quiere utilizar, deberemos acompañarlo del método quote(). Para indicar que queremos escapar (anteponer '\' a caracteres problemáticos) un valor. Es similar a utilizar mysqli_real_escape_string() o real_escape_string() si se utiliza con la interfaz orientada a objetos.

 try  
 {  
      $con = new PDO('mysql:dbname=demo';host=localhost', $user, $password);  
      $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);  
      $stmt = $con->query('SELECT nombre FROM usuario WHERE nombre like ' . $con->quote($cad));  
 }  
 catch(PDOException $e)  
 {  
      echo 'Error: ' . $e->getMessage();  
 }  

4) Consultas preparadas

El método necesario para preparar consultas preparadas es prepare(). Se encarga automáticamente de escapar los parámetros que se pasen, evitándonos problemas con la inyección de SQL. Esta función nos devolverá un PDOStatement sobre el cual ejecutaremos la función fetch() para poder mostrar su valor. Pero antes vamos a ver como pasar parámetros.
Importante es saber que prepare() nos va a devolver un objeto  PDOStatement en caso de que no haya error al preparar la consulta (False en caso de error). Y este objeto es el que vamos a utilizar para realizar las posteriores vinculaciones, ejecuciones y recogida de resultados.

Los parámetros de entrada de datos irán precedidos por (:) y posteriormente el nombre del parámetro de sustitución.

 $stmt = $con->prepare('SELECT * FROM tabla WHERE Nombre=:campo_nombre and Apellido1=:campo_apellido');  

Otra alternativa es usar (?) para los parámetros de entrada

 $stmt = $con->prepare('SELECT * FROM tabla WHERE Nombre=? and Apellido1=?');  

Utilicemos la alternativa que queramos, el siguiente paso es vincular un valor a los parámetros de sustitución. Y para ello utilizaremos la función bindValue() o bindParam().

 public bool PDOStatement::bindValue ( mixed $parameter , mixed $value [, int $data_type = PDO::PARAM_STR ] )  

 public bool PDOStatement::bindParam ( mixed $parameter , mixed &$variable [, int $data_type = PDO::PARAM_STR [, int $length [, mixed $driver_options ]]] )  

donde $data_type (parámetro opcional) puede ser:

- PDO::PARAM_BOOL
- PDO::PARAM_NULL
- PDO::PARAM_INT
- PDO::PARAM_STR (por defecto)
- PDO::PARAM_LOB

Por lo tanto si hemos utilizado la sintaxis (:)

 $stmt->bindValue(':campo_nombre', $nombre);  
 $stmt->bindValue(':campo_apellido', $apellido);  

O si hemos optado por la sintaxis (?).

 $stmt->bindValue(1, $nombre);  
 $stmt->bindValue(2, $apellido);  

Como observaremos el primer parámetro ($parameter) en este caso se indica con la posición en la consulta del campo a vincular. A diferencia de si usamos la sintaxis (:) donde si especificamos el campo a vincular en la consulta.

Las funciones bindParam() y bindValue() son muy similares, salvo por:
- Con bindParam() sólo se puede pasar variables y no valores directamente. Sólo funciona con las variables ya que solo permite que parámetros  por "referencia" (y un valor que no es una "referencia" válida en PHP).
- Con bindValue se puede pasar tanto valores como variables.

5) Ejecución

Solo si hemos utilizado una consulta preparada, necesitamos ejecutar nuestra consulta con la siguiente función antes de poder recuperar los resultados. Si la consulta se realiza con query(), y por lo tanto no es preparada, podremos directamente recoger los datos con una de las funciones del aparatado 6.

 public bool PDOStatement::execute ([ array $input_parameters ] )  

Una característica importante de esta función es que permite opcionalmente introducir los parámetros a vincular. Por lo tanto si no queremos hacer uso de las funciones de vinculación bindParam() o bindValue(), podemos pasar los parámetros directamente a esta función.

 $stmt = $gbd->prepare('SELECT name, colour, calories  
   FROM fruit  
   WHERE calories < :calories AND colour = :colour');  
 $stmt->execute(array(':calories' => $calorías, ':colour' => $color));  

Esta función devuelve un objeto True o False en caso de éxito o de error a realizar la consulta.

6) Recogida de datos

Como hemos comentado cuando presentamos prepare() o query(), tenemos un objeto de tipo  PDOStatement ($stmt), que hemos utilizando para las vinculaciones y ejecuciones en el caso de consultas preparadas. Y para recuperar el resultado de la ejecución de una consulta también haremos uso de él.

En MySQLi teniamos 3 funciones (tanto para interfaz orientada a objetos como para la interfaz procedimental) para obtener los resultado de un objeto mysqli_result:
- array mysqli_result::fetch_assoc ( void ) / array mysqli_fetch_assoc ( mysqli_result $result )

- mixed mysqli_result::fetch_array ( [ int $tiporesultado = MYSQLI_BOTH ] ) /
  mixed mysqli_fetch_array ( mysqli_result $result [, int $tiporesultado = MYSQLI_BOTH ] )

- object mysqli_result::fetch_object ([ string $nombre_clase [, array $params ]]) /
   object mysqli_fetch_object ( mysqli_result $result [, string $class_name [, array $params ]] )

Y con PDOStatement tenemos también 3 métodos principales para acceder al resultado de una ejecución. Aunque los siguientes métodos tienen más parámetros vamos a presentar los principales. Para más información mirar en http://www.php.net.

A) Obtener la siguiente fila de un conjunto de resultados. Por lo que tendremos que utilizar un bucle para recorrer todos los resultados (filas) de la ejecución de la consulta.

 public mixed PDOStatement::fetch ([ int $fetch_style])  

Dependiendo dependiendo del valor del parámetro que especifiquemos, el resultado será de un tipo u otro. Aunque habitualmente solo utilizaremos uno de los 3 siguientes valores, que devuelven el resultado en forma de array:
- PDO::FETCH_ASSOC: devuelve un array indexado por los nombres de las columnas del conjunto de resultados.
- PDO::FETCH_NUM: devuelve un array indexado por el número de columna tal como fue devuelto en el conjunto de resultados, comenzando por la columna 0.
- PDO::FETCH_BOTH (predeterminado): devuelve un array indexado tanto por nombre de columna, como numéricamente con índice de base 0 tal como fue devuelto en el conjunto de resultados.

Ejemplo

 //...  
 $stmt = $con->prepare("SELECT id, nombre, email FROM usuario");  
 $stmt->execute();  
 /* Prueba de tipos de PDOStatement::fetch */  
 echo "PDO::FETCH_ASSOC: ";  
 echo "Devolver la siguiente fila como un array indexado por nombre de colunmna\n";  
 $result = $stmt->fetch(PDO::FETCH_ASSOC);  
 print_r($result);  
 echo "\n";  
 echo "PDO::FETCH_NUM: ";  
 echo "Devolver la siguiente fila como un array indexado por número de columna\n";  
 $result = $stmt->fetch(PDO::FETCH_BOTH);  
 print_r($result);  
 echo "\n";  
 echo "PDO::FETCH_BOTH: ";  
 echo "Devolver la siguiente fila como un array indexado por nombre y número de columna\n";  
 $result = $stmt->fetch(PDO::FETCH_BOTH);  
 print_r($result);  
 echo "\n";  

Salida

 PDO::FETCH_ASSOC: Devolver la siguiente fila como un array indexado por nombre de colunmna  
 Array  
 (  
   [id] => 1  
   [nombre] => jose  
   [email] => prueba@ejemplo.com  
 )  
 PDO::FETCH_NUM: Devolver la siguiente fila como un array indexado por número de columna";   
 Array  
 (  
   [0] => 1  
   [1] => jose  
   [2] =>prueba@ejemplo.com  
 )  
 PDO::FETCH_BOTH: Devolver la siguiente fila como un array indexado por nombre y número de columna  
 Array  
 (  
   [id] => 1  
   [0] => 1  
   [nombre] => jose  
   [1] => jose  
   [email] =>prueba@ejemplo.com  
   [2] =>prueba@ejemplo.com  
 )  

Ejemplo con query() y bucle para obtener todos los resultados.

 $sql = "SELECT * FROM usuario";  
 $stmt= $conn->query($sql);  
 while($fila = $stmt->fetch(PDO::FETCH_ASSOC)) {  
    echo $fila['id']. ' - '. $fila['nombre']. ' - '. $fila['email']. ' - '. $fila['direccion']. '<br />';  
  }  

B) Obtener un array con todas las filas del resultado. Mientras que con la anterior función obteníamos un array con los campos de una fila. Ahora una matriz (array de arrays) de dos dimensiones.

 public array PDOStatement::fetchAll ([ int $fetch_style])  

Al igual que con la función anterior fetch() dependiendo de el valor que pasemos como $fetch_style el array tendrá una forma u otra. Nuevamente FETCH_BOTH es el valor por defecto.

 $stmt = $con->prepare("SELECT id, nombre, emial FROM usuario");  
 $stmt->execute();  
 /* Usaremos el estilo por defecto en la función fetch_all */  
 print("Obtener todas las filas restantes del conjunto de resultados:\n");  
 $resultado = $stmt->fetchAll();  
 print_r($resultado);  

 Obtener todas las filas restantes del conjunto de resultados:  
 Array  
 (  
   [0] => Array  
     (  
         [id] => 1  
         [0] => 1  
         [nombre] => jose  
         [1] => jose  
         [email] =>prueba@ejemplo.com  
         [2] =>prueba@ejemplo.com  
     )  
   [1] => Array  
     (  
         [id] => 2  
         [0] => 2  
         [nombre] => juan  
         [1] => juan  
         [email] =>prueba2@ejemplo.com  
         [2] =>prueba2@ejemplo.com  
     )  
 )  

C) Obtener la siguiente fila y devolverla como un objeto.

Al igual que pasaba con fetch_object de la extensión mysqli, podemos (primer parámetro opcional) pasar los datos de la fila a un objeto y así llamar a métodos de la clase instanciada para trabajar con ellos. Además podríamos (segundo parámetro opcional) pasar datos al constructor de la clase.
Los campos de la fila del conjunto resultado se agregan automáticamente como atributos públicos de la clase. Por lo tanto podremos acceder a ellos dentro de la clase, sin tenerlos que definir. Pero es una buena practica definirlos en la clase. Y si se definen, tienen que coincidir con el nombre de la columna.

 public mixed PDOStatement::fetchObject ([ string $class_name = "stdClass" [, array $ctor_args ]] )  

Si no instanciamos ninguna clase simplemente accederemos a las columna de la fila como si esta fuera un objeto y las columnas propiedades.

 class animal{  
   public $id;  
   public $tipo;  
   public $nombre;  
   public function capitalizarTipo() {  
     return ucwords($this->tipo);  
   }  
 }  
 try {  
   $dbh = new PDO("mysql:host=$hostname;dbname=zoo", $username, $password);  
   $sql = "SELECT * FROM animales";  
   $stmt = $dbh->query($sql);  
   while ($animal = $stmt->fetchObject('animal')) {  
     echo $animal->id . '<br />';  
     echo $animal->capitalizarTipo() . '<br />';  
     echo $animal->nombre;  
   }  
 } catch (PDOException $e) {  
   echo $e->getMessage();  
 }  


Entradas relacionadas

PDO - Parte 2

1 comentario: