La extensión PDO (PHP Data Objects) permite acceder a distintas bases de datos (MySQL/MariaBD, PostgreSQL, Oracle, MS SQL Server, SQLite, Firebird, DB2, Informix, etc.) de una forma uniforme, lo que facilita la portabilidad de las aplicaciones. La portabilidad entre bases de datos no es absoluta debido por ejemplo a las diferencias entre diferentes bases de datos en aspectos como la forma de conectarse o la sintaxis del lenguaje de definición de datos. Para ilustrar esas diferencias y como tratarlas adecuadamente, se ofrecen ejemplos de código específicos para SQLite y para MySQL.
La extensión PDO no evalúa la corrección de las consultas SQL, aunque sí implementa algunas medidas de seguridad mediante consultas preparadas, como se comenta en el apartado correspondiente.
PDO es una biblioteca de PHP orientada a objetos, pero se puede utilizar sin problemas en un programa no orientado a objetos. Tan solo será necesario utilizar la notación propia de la orientación orientada a objetos al manipular el objeto PDO.
Para trabajar con una base de datos en nuestro programa, simplemente nos conectaremos con la base de datos creando un objeto PDO. La información necesaria para la conexión con la base de datos específica se incluye como uno o varios argumentos:
// CONEXIÓN CON LA BASE DE DATOS
$pdo = new PDO("informacion-para-la-conexion");
La información para la conexión consiste en primer lugar en el DSN (Data Source Name), que indica como mínimo el nombre de la base de datos (sqlite, mysql, pgsql, etc.) y su ubicación. En su caso, el segundo y tercer argumento son el nombre y contraseña del usuario de la base de datos.
Si por algún motivo PDO no puede conectarse con la base de datos, se genera una excepción. En los ejemplos y soluciones de ejercicios de estos apuntes cuando se produce esta excepción se muestra un mensaje en español y a continuación el código de error SQLSTATE y el mensaje de error específico del driver.
print " <p class=\"aviso\">Error: No puede conectarse con la base de datos. {$e->getMessage()}</p>\n";
PDO permite elegir al usuario entre tres formas distintas de gestionar los errores encontrado al realizar las consultas (normalmente por errores de sintaxis de SQL):
En este modo, los errores de PDO no interrumpen la ejecución del programa, simplemente se guardan y se pueden consultar mediante los métodos errorCode() y errorInfo. Si utilizamos este modo, nuestro programa tiene que encargarse de comprobar tras cada consulta si ha habido un error y tenerlo en cuenta.
En este modo, PDO además de guardar los errores genera un aviso de tipo E_WARNING que, dependiendo de la configuración de error_reporting, se mostrará o no en pantalla.
En este modo, PDO genera una excepción de clase PDOException. Si utilizamos este modo, nuestro programa tiene que gestionar las excepciones, porque si no el programa se interrumpe con un error E_ERROR.
Hasta PHP 8.0, publicado en 2020, el modo predeterminado era ERRMODE_SILENT, pero desde esa versión es ERRMODE_EXCEPTION. En estos apuntes se utiliza el modo PDO::ERRMODE_SILENT, que se puede abordar con simples estructuras if .. else ..., mientras que el modo PDO::ERRMODE_EXCEPTION requiere estructuras try ... catch ... que no se utilizan en estos apuntes (excepto en la conexión con las bases de datos).
El modo de manejo de errores se selecciona activando el atributo PDO::ATTR_ERRMODE tras conectarse con la base de datos, como se comenta más adelante en esta lección en el apartado dedicado a la conexión con la base de datos:
$tmp->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
Una vez realizada la conexión, las consultas SQL se realizan a través del objeto PDO. La ejecución de la consulta y el tratamiento de la respuesta se puede hacer de cuatro formas distintas, atendiendo a dos criterios:
Por un lado, se debe considerar si la consulta incluye información que nos ha llegado a través de un formulario. Por otro lado, se debe considerar si la consulta puede devolver registros:
// EJECUCIÓN DE UNA CONSULTA SIN DATOS PROVENIENTES DE UN FORMULARIO
$pdo->query("consulta");
// EJECUCIÓN DE UNA CONSULTA QUE NO VA A DEVOLVER REGISTROS
if (!$pdo->query("consulta")) {
... // Si la consulta falla
} else {
... // Si la consulta se ejecuta correctamente
}
// EJECUCIÓN DE UNA CONSULTA QUE PUEDE DEVOLVER REGISTROS
$resultado = $pdo->query("consulta");
if (!$resultado) {
... // Si la consulta falla
} else {
... // Si la consulta se ejecuta correctamente
}
// EJECUCIÓN DE UNA CONSULTA CON DATOS PROVENIENTES DE UN FORMULARIO
$resultado = $pdo->prepare("consulta");
$resultado->execute([parámetros de la consulta]);
En este caso, independientemente de que la consulta devuelva a o no registros, debemos utilizar siempre una variable auxiliar y escribir la estructura if ... else ... a partir de esa variable auxiliar, puesto que debemos comprobar si ha fallado tanto la preparación de la consulta como la ejecución de la sentencia preparada:
// EJECUCIÓN DE UNA CONSULTA PREPARADA
$resultado = $pdo->prepare("consulta");
if (!$resultado) {
... // Si la preparación de la consulta falla
} elseif (!$resultado->execute([parámetros de la consulta]) {
... // Si la ejecución de la consulta falla
} else {
... // Si la consulta se ejecuta correctamente
}
Cuando la consulta falle, es conveniente mostrar al usuario el motivo del fallo. Para ello, podemos utilizar los métodos errorCode() y errorInfo(). errorCode() devuelve el código de error SQLSTATE definido en la norma ANSI SQL. errorInfo() devuelve una matriz con el código de error SQLSTATE, el código de error específico del driver y el mensaje de error específico del driver.
En los ejemplos y soluciones de ejercicios de estos apuntes cuando se produce un error se muestra un mensaje en español y a continuación el código de error SQLSTATE y el mensaje de error específico del driver.
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
En los ejemplos anteriores se ha escrito la consulta en la llamada al método, pero es habitual definir una variable $consulta que contenga la consulta y utilizar esa variable en la llamada al método:
// En una sola línea de programa
$resultado = $pdo->query("SELECT * FROM tabla");
// En dos líneas de programa
$consulta = "SELECT * FROM tabla";
$resultado = $pdo->query($consulta);
Utilizar una variable auxiliar permite por ejemplo imprimir la consulta mientras se está programando para comprobar que no tiene errores.
$consulta = "SELECT * FROM tabla";
print "<p>Consulta: $consulta</p>\n";
// var_dump($consulta);
$resultado = $pdo->query($consulta);
Cuando la consulta puede devolver registros, se debe guardar la respuesta en una variable auxiliar. Esa variable es de un tipo especial llamado recurso que no se puede acceder directamente, pero de la que podemos ir extrayendo uno a uno los registros mediante un bucle foreach () o mediante los métodos fetch() o fetchColumn():
// CONSULTA DE SELECCIÓN DE REGISTROS QUE DEVUELVE UN ÚNICO REGISTRO DE UNA COLUMNA
$consulta = "SELECT COUNT(*) FROM tabla";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>La tabla contiene {$resultado->fetchColumn()} registro(s).</p>\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE DEVUELVE UN ÚNICO REGISTRO (O NINGUNO)
$id = 1; // Normalmente este valor vendrá de un formulario
$consulta = "SELECT * FROM tabla WHERE id=:id"; // Normalmente solo habrá un registro (o ninguno) con un id determinado
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
$registro = $resultado->fetch();
print " <p><strong>Registro obtenido</strong></p>\n";
print "\n";
print " <p>$registro[columna1] - $registro[columna2] - etc.</p>\n";
print "\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE PUEDE DEVOLVER VARIOS REGISTROS (O UNO O NINGUNO)
$consulta = "SELECT * FROM tabla";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p><strong>Registro(s) obtenido(s)</strong></p>\n";
print " <ul>\n";
foreach ($resultado as $registro) {
print " <li>$registro[columna1] - $registro[columna2] - etc.</li>\n";
}
print " </ul>\n";
print "\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE PUEDE DEVOLVER VARIOS REGISTROS (O UNO O NINGUNO)
$consulta = "SELECT * FROM tabla";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (count($registros = $resultado->fetchAll()) == 0) {
print " <p class=\"aviso\">No se ha creado todavía ningún registro.</p>\n";
} else {
print " <p><strong>Registro(s) obtenido(s)</strong></p>\n";
print " <ul>\n";
foreach ($registros as $registro) {
print " <li>$registro[columna1] - $registro[columna2] - etc.</li>\n";
}
print " </ul>\n";
print "\n";
}
Para desconectar con la base de datos, simplemente hay que destruir el objeto PDO.
$pdo = null;
Aunque no se destruya explícitamente el objeto PDO, PHP cierra la conexión al terminar de ejecutar la página, por lo que normalmente la instrucción no se suele escribir.
Para evitar ataques de inyección SQL (en la lección Inyecciones SQL se comentan los ataques más elementales), se recomienda el uso de sentencias preparadas, en las que PDO se encarga de "desinfectar" los datos. En general, cualquier consulta que incluya datos provenientes de un formulario (o en general, provenientes del usuario de la aplicación) debe realizarse mediante consultas preparadas.
El método para efectuar consultas preparadas es primero preparar la consulta con PDO->prepare($consulta) y después ejecutarla con PDO->execute([parámetros]), que devuelve el resultado de la consulta.
// Consulta preparada
$consulta = "SELECT * FROM tabla";
$resultado = $pdo->prepare($consulta);
$resultado->execute();
Si el programa incluye directamente en la consulta los datos recibidos a través de un formulario, PDO no puede hacer ninguna "desinfección" de los datos, ya que PDO no puede identificar qué parte de la consulta es la que ha llegado de un formulario y que puede haber manipulado la consulta original:
$valor1 = $_REQUEST["valor1"];
$valor2 = $_REQUEST["valor2"];
// ¡CUIDADO! DESACONSEJADO: PDO NO PUEDE DESINFECTAR LOS DATOS
$consulta = "SELECT COUNT(*) FROM tabla
WHERE columna1=$valor1
AND columna2=$valor2";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute();) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Se han encontrado {$resultado->fetchColumn()} registro(s).</p>\n";
print "\n";
}
Para que PDO pueda desinfectar los datos provinientes de un formulario, estos deben enviarse al ejecutar la consulta, no al prepararla. Para ello, al definir la consulta es necesario indicar la posición de los datos y al ejecutar la consulta es necesario indicar el valor de los datos en forma de matriz. La posición de los datos se puede indicar de dos maneras distintas, mediante parámetros o mediante interrogantes (se aconseja la utilización de parámetros para evitar errores).
En este caso las posiciones en la que se insertarán los datos provenientes del formulario se indican mediante cadenas de texto precedidas de dos puntos (:). La matriz con los datos debe incluir los nombres de los parámetros y los valores que sustituyen a los parámetros (el orden no es importante), como muestra el siguiente ejemplo:
$valor1 = $_REQUEST["valor1"];
$valor2 = $_REQUEST["valor2"];
// CONSULTA PREPARADA CON PARÁMETROS: PDO PUEDE DESINFECTAR LOS DATOS
$consulta = "SELECT COUNT(*) FROM tabla
WHERE columna1=:valor1
AND columna2=:valor2";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":valor1" => $valor1, ":valor2" => $valor2]);) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Se han encontrado {$resultado->fetchColumn()} registro(s).</p>\n";
print "\n";
}
En este caso las posiciones en la que se insertarán los datos provenientes del formulario se indican mediante interrogantes (?). La matriz con los datos debe incluir los valores que sustituyen a los interrogantes (en el mismo orden en que aparecen en la consulta), como muestra el siguiente ejemplo:
$valor1 = $_REQUEST["valor1"];
$valor2 = $_REQUEST["valor2"];
// CONSULTA PREPARADA CON INTERROGANTES: PDO PUEDE DESINFECTAR LOS DATOS
$consulta = "SELECT COUNT(*) FROM tabla
WHERE columna1=?
AND columna2=?";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([$valor1, $valor2]);) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Se han encontrado {$resultado->fetchColumn()} registro(s).</p>\n";
print "\n";
}
Notas:
Debido a que las consultas preparadas se idearon para optimizar el rendimiento de las consultas, el uso de parámetros tiene algunas restricciones. Por ejemplo
Si no podemos usar parámetros, no queda más remedio que incluir los datos en la consulta. Como en ese caso PHP no hace ninguna desinfección de los datos, la desinfección la tenemos que hacer nosotros previamente.
Como en estos casos los valores introducidos por el usuario suelen tener unos valores restringidos (por ejemplo, si el usuario puede elegir una columna de una tabla, los nombres de las columnas están determinadas y el usuario sólo puede elegir uno de ellos), podemos utilizar la función recoge() con los parámetros opcionales default y allowed que restringe los valores admitidos a los indicados. De esa manera impedimos los ataques de inyección por parte del usuario, como muestra el siguiente ejemplo:
// EJEMPLO DE RESTRICCIONES EN LOS PARÁMETROS DE CONSULTAS PREPARADAS
// Matriz con los nombres válidos de las columnas de la tabla
$columnas = [
"columna1",
"columna2",
...
];
$columna = recoge("columna", default: "columna1", allowed: $columnas);
$valor1 = $_REQUEST["valor1"];
// CONSULTA PREPARADA CON PARÁMETROS: PDO PUEDE DESINFECTAR LOS DATOS
$consulta = "SELECT * FROM tabla
WHERE columna1=:valor1
ORDER BY $columna ASC";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":valor1" => $valor1]);) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
...
A continuación se ofrecen ejemplos de código de cómo se podría trabajar con PDO para realizar las tareas más habituales de gestión de una base de datos.
La base de datos de estos ejemplos tiene una estructura muy simple: una sola tabla personas con tres columnas: un identificador numérico (id), el nombre y los apellidos de una persona.
La estructura general de estas consultas se repite con ligeras variaciones. La estructura concreta viene impuesta por dos aspectos:
Para dotar de flexibilidad a la aplicación, la aplicación incluirá opciones de configuración en forma de variables. Distinguiremos entre opciones disponibles para el administrador de la aplicación y opciones disponibles para el programador. Aunque la distinción es a veces algo arbitraria, el objetivo es ayudar al administrador de la aplicación en su puesta en marcha:
Estas podrían ser las opciones para SQLite:
// SQLITE: OPCIONES DE CONFIGURACIÓN DEL PROGRAMA
// VARIABLES CONFIGURABLES POR EL ADMINISTRADOR DE LA APLICACIÓN
// Configuración para SQLite
$cfg["sqliteDatabase"] = "/tmp/db.sqlite"; // Ubicación de la base de datos
Estas podrían ser las opciones para MySQL:
// MYSQL: OPCIONES DE CONFIGURACIÓN DEL PROGRAMA
// VARIABLES CONFIGURABLES POR EL ADMINISTRADOR DE LA APLICACIÓN
// Configuración para MySQL
$cfg["mysqlHost"] = "localhost"; // Nombre de host
$cfg["mysqlUser"] = ""; // Nombre de usuario
$cfg["mysqlPassword"] = ""; // Contraseña de usuario
$cfg["mysqlDatabase"] = ""; // Nombre de la base de datos
Estas podrían ser las opciones comunes a ambas bases de datos:
// Variables configurables por el programador de la aplicación
// Nombres de las tablas
$cfg["tablaPersonas"] = "personas"; // Nombre de la tabla Personas
// Variables configurables por el administrador de la aplicación
// Tamaño de los campos en la tabla Personas
$cfg["tablaPersonasTamNombre"] = 40; // Tamaño de la columna Personas > Nombre
$cfg["tablaPersonasTamApellidos"] = 60; // Tamaño de la columna Personas > Apellidos
Para conectar con la base de datos hay que crear una instancia de la clase PDO, que se utiliza en todas las consultas posteriores. En cada página php que incluya consultas a la base de datos es necesario conectar primero con la base de datos.
Si no se puede establecer la conexión con la base de datos, puede deberse a que la base de datos no esté funcionando, a que los datos de usuario no sean correctos, a que no esté activada la extensión pdo o (en el caso de SQLite) que el camino donde se quiere guardar la base de datos no exista.
Para poder utilizar SQLite mediante PDO, debe estar activada la extensión pdo_sqlite en el archivo de configuración php.ini (véase el apartado extensión pdo_sqlite en la lección de configuración de Apache y PHP).
En SQLite, no se hace una conexión a un servidor, sino que simplemente se indica el archivo que va a contener la base de datos. En SQLite no hay un servidor que gestiona todas las bases de datos, sino que cada base de datos es un archivo independiente.
Tenga en cuenta que PDO puede crear el archivo SQLite, pero no puede crear la ruta hasta el archivo, que debe existir previamente.
// SQLITE: Conexión con la base de datos
function conectaDb()
{
global $cfg;
try {
$tmp = new PDO("sqlite:$cfg[sqliteDatabase]");
$tmp->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$tmp->query("PRAGMA foreign_keys = ON");
$tmp->query("PRAGMA encoding = 'UTF-8'");
return $tmp;
} catch (PDOException $e) {
print " <p class=\"aviso\">Error: No puede conectarse con la base de datos. {$e->getMessage()}</p>\n";
exit;
}
}
// CONEXIÓN CON LA BASE DE DATOS
// La conexión se debe realizar en cada página que acceda a la base de datos
$pdo = conectaDb();
Notas:
En el caso de MySQL, para crear el objeto PDO se necesita proporcionar el nombre del servidor, el nombre de usuario de la base de datos y la contraseña. En el ejemplo siguiente esos datos se proporcionan como constantes que deberían definirse en el programa. El usuario de la base de datos se debe crear previamente.
Para poder acceder a MySQL mediante PDO, debe estar activada la extensión php_pdo_mysql en el archivo de configuración php.ini (véase el apartado extensión pdo_mysql en la lección de configuración de Apache y PHP).
// MYSQL: Conexión con la base de datos
function conectaDb()
{
global $cfg;
try {
$tmp = new PDO("mysql:host=$cfg[mysqlHost];dbname=$cfg[mysqlDatabase];charset=utf8mb4", $cfg["mysqlUser"], $cfg["mysqlPassword"]);
} catch (PDOException $e) {
$tmp = new PDO("mysql:host=$cfg[mysqlHost];charset=utf8mb4", $cfg["mysqlUser"], $cfg["mysqlPassword"]);
} catch (PDOException $e) {
print " <p class=\"aviso\">Error: No puede conectarse con la base de datos. {$e->getMessage()}</p>\n";
exit;
} finally {
$tmp->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$tmp->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
return $tmp;
}
}
// EJEMPLO DE USO DE LA FUNCIÓN conectaDb()
// La conexión se debe realizar en cada página que acceda a la base de datos
$pdo = conectaDb();
Notas:
Las consultas de creación no son iguales en MySQL y SQLite. Para que una misma aplicación pueda trabajar con una u otra base de datos, en el apartado aplicación configurable estas consultas se incluyen en bibliotecas específicas.
Con SQLite no tiene sentido crear o borrar la base de datos, ya que SQLite trabaja con ficheros y cada fichero es una base de datos. Tan solo es necesario borrar y crear las tablas.
Para borrar una tabla, se utiliza la consulta DROP TABLE. Esta consulta no es específica de SQlite y funcionaría con otras bases de datos. Se ha añadido la condición IF EXISTS porque en algunas versiones de SQLite la consulta DROP TABLE da error si la tabla no existe.
// CONSULTA DE BORRADO DE TABLA
$consulta = "DROP TABLE IF EXISTS $cfg[tablaPersonas]";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al borrar la tabla. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla borrada correctamente (si existía).</p>\n";
}
print "\n";
Para crear una tabla, se utiliza la consulta CREATE TABLE. Las consultas de creación de tabla suelen ser específicas de cada base de datos. El ejemplo no utiliza sentencias preparadas (en caso de utilizarse sentencias preparadas, las variables no podrían ir como parámetros por tratarse de sentencias DDL).
// SQLITE: CONSULTA DE CREACIÓN DE TABLA
$consulta = "CREATE TABLE $cfg[tablaPersonas] (
id INTEGER PRIMARY KEY,
nombre VARCHAR($cfg[tablaPersonasTamNombre]),
apellidos VARCHAR($cfg[tablaPersonasTamApellidos])
)";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la tabla. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla creada correctamente.</p>\n";
}
print "\n";
Con MySQL, borraremos la base de datos (y las tablas existentes), crearemos de nuevo la base de datos y crearemos la tabla.
Para borrar una base de datos, se utiliza la consulta DROP DATABASE. Se ha añadido la condición IF EXISTS porque en algunas bases de datos la consulta DROP DATABASE da error si la tabla no existe.
// MYSQL: CONSULTA DE BORRADO DE BASE DE DATOS
$consulta = "DROP DATABASE IF EXISTS $cfg[mysqlDatabase]";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al borrar la base de datos. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Base de datos borrada correctamente (si existía).</p>\n";
}
print "\n";
Para crear una base de datos, se utiliza la consulta CREATE DATABASE. En el caso de MySQL es necesario indicar con una consulta USE que se desea trabajar con esa base de datos (independientemente de que nos hayamos conectado indicando el nombre de la base de datos en el DSN o no).
// MYSQL: CONSULTA DE CREACIÓN DE BASE DE DATOS
$consulta = "CREATE DATABASE $cfg[mysqlDatabase]
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la base de datos. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Base de datos creada correctamente.</p>\n";
print "\n";
$consulta = "USE $cfg[mysqlDatabase]";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Base de datos seleccionada correctamente.</p>\n";
print "\n";
}
}
Nota: El juego de caracteres utilizado en este curso es UTF-8, por lo que en la base de datos MySQL se utiliza el juego de caracteres utf8mb4 (que permite almacenar cualquier carácter Unicode) y el cotejamiento utf8mb4_unicode_ci (que implementa todos los criterios de ordenación de Unicode). Para una explicación más detallada se puede consultar el blog de Mathias Bynens.
Para crear una tabla, se utiliza la consulta CREATE TABLE. Las consultas de creación de tabla suelen ser específicas de cada base de datos. El ejemplo no utiliza sentencias preparadas (en caso de utilizarse sentencias preparadas, las variables no podrían ir como parámetros por tratarse de sentencias DDL).
// MYSQL: CONSULTA DE CREACIÓN DE TABLA
$consulta = "CREATE TABLE $cfg[tablaPersonas] (
id INTEGER UNSIGNED AUTO_INCREMENT,
nombre VARCHAR($cfg[tablaPersonasTamNombre]),
apellidos VARCHAR($cfg[tablaPersonasTamApellidos]),
PRIMARY KEY(id)
)";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la tabla. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla creada correctamente.</p>\n";
}
Estas consultas son iguales en MySQL y SQLite.
// CONSULTA DE INSERCIÓN DE REGISTRO
$nombre = "Pepito"; // Normalmente estos valores vendrán de un formulario
$apellidos = "Conejo";
$consulta = "INSERT INTO $cfg[tablaPersonas]
(nombre, apellidos)
VALUES (:nombre, :apellidos)";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":nombre" => $nombre, ":apellidos" => $apellidos])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro creado correctamente.</p>\n";
print "\n";
}
// CONSULTA DE MODIFICACIÓN DE REGISTRO
$id = 1; // Normalmente estos valores vendrán de un formulario
$nombre = "Pepita";
$apellidos = "Conejo";
$consulta = "UPDATE $cfg[tablaPersonas]
SET nombre=:nombre, apellidos=:apellidos
WHERE id=:id";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":nombre" => $nombre, ":apellidos" => $apellidos, ":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro modificado correctamente.</p>\n";
print "\n";
}
Nota: En el ejemplo siguiente los registros a borrar se reciben en forma de matriz y se recorre la matriz borrando un elemento en cada iteración.
// CONSULTA DE BORRADO DE REGISTROS
$id = [1 => "on", 3 => "on"]; // Normalmente este valor vendrá de un formulario (en este caso, como matriz).
foreach ($id as $indice => $valor) {
$consulta = "DELETE FROM $cfg[tablaPersonas]
WHERE id=:indice";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":indice" => $indice])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro borrado correctamente (si existía).</p>\n";
print "\n";
}
}
Si el campo es de tipo numérico, y el valor a insertar es una cadena (como ocurre cuando recogemos el dato de un formulario), podríamos tener un problema de tipos al insertarlo. En el caso de los sistemas gestores de bases de datos que se usan en este curso (SQLite y MariaDB/MySQL de forma general, PostgreSQL en algún ejercicio) la conversión de tipos es automática y la cadena se convierte automáticamente en un valor numérico, pero puede que en otros sistemas la conversión no se realice y se produzca un error.
Si nos encontráramos con este problema, deberíamos realizar la conversión de cadena a entero. Para ello podríamos usar la función de PHP intval() como muestra el ejemplo siguiente en el que "edad" sería un campo INTEGER:
$consulta = "INSERT INTO $cfg[tablaPersonas]
(nombre, edad)
VALUES (:nombre, :edad)";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":nombre" => $nombre, ":edad" => intval($edad)])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro creado correctamente.</p>\n";
}
En las soluciones de los ejercicios de estos apuntes no se realiza la conversión, ya que SQLite, MySQL o PostgreSQL no lo necesitan.
Para obtener registros que cumplan determinados criterios se utiliza una consulta SELECT. Estas consultas son iguales en MySQL y SQLite.
// CONSULTA DE SELECCIÓN DE REGISTROS QUE DEVUELVE UN ÚNICO REGISTRO DE UNA COLUMNA
$consulta = "SELECT COUNT(*) FROM $cfg[tablaPersonas]";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>La tabla contiene {$resultado->fetchColumn()} registro(s).</p>\n";
print "\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE DEVUELVE UN ÚNICO REGISTRO (O NINGUNO)
$id = 1; // Normalmente este valor vendrá de un formulario
$consulta = "SELECT * FROM $cfg[tablaPersonas] WHERE id=:id";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
$registro = $resultado->fetch();
print " <p><strong>Registro obtenido</strong></p>\n";
print "\n";
print " <p>$registro[id] - $registro[nombre] - $registro[apellidos].</p>\n";
print "\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE PUEDE DEVOLVER VARIOS REGISTROS (O UNO O NINGUNO)
$consulta = "SELECT * FROM $cfg[tablaPersonas]";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p><strong>Registro(s) obtenido(s)</strong></p>\n";
print " <ul>\n";
foreach ($resultado as $registro) {
print " <li>$registro[id] - $registro[nombre] - $registro[apellidos]</li>\n";
}
print " </ul>\n";
print "\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE PUEDE DEVOLVER VARIOS REGISTROS (O UNO O NINGUNO)
$consulta = "SELECT * FROM $cfg[tablaPersonas]";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p><strong>Registro(s) obtenido(s)</strong></p>\n";
print " <ul>\n";
while ($registro = $resultado->fetch()) {
print " <li>$registro[id] - $registro[nombre] - $registro[apellidos]</li>\n";
print "\n";
}
print " </ul>\n";
print "\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE PUEDE DEVOLVER VARIOS REGISTROS (O UNO O NINGUNO)
$consulta = "SELECT COUNT(*) FROM $cfg[tablaPersonas]";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif ($resultado->fetchColumn() == 0) {
print " <p>No se ha creado todavía ningún registro en la tabla.</p>\n";
} else {
$consulta = "SELECT * FROM $cfg[tablaPersonas]";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p><strong>Registro(s) obtenido(s)</strong></p>\n";
print " <ul>\n";
foreach ($resultado as $registro) {
print " <li>$registro[id] - $registro[nombre] - $registro[apellidos]</li>\n";
}
print " </ul>\n";
print "\n";
}
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE PUEDE DEVOLVER VARIOS REGISTROS (O UNO O NINGUNO)
$consulta = "SELECT * FROM $cfg[tablaPersonas]";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (count($registros = $resultado->fetchAll()) == 0) {
print " <p class=\"aviso\">No se ha creado todavía ningún registro.</p>\n";
} else {
print " <p><strong>Registro(s) obtenido(s)</strong></p>\n";
print " <ul>\n";
foreach ($registros as $registro) {
print " <li>$registro[id] - $registro[nombre] - $registro[apellidos]</li>\n";
}
print " </ul>\n";
print "\n";
}
La consulta SELECT permite efectuar búsquedas en cadenas utilizando el condicional LIKE o NOT LIKE y los comodines _ (cualquier carácter) o % (cualquier número de caracteres). Estas consultas son iguales en MySQL y SQLite.
Ejemplos de consultas:
// CONSULTA DE SELECCIÓN DE REGISTROS QUE DEVUELVE UN ÚNICO REGISTRO DE UNA COLUMNA
$apellidos = "nej"; // Normalmente este valor vendrá de un formulario
$consulta = "SELECT COUNT(*) FROM $cfg[tablaPersonas]
WHERE apellidos LIKE :apellidos";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":apellidos" => "%$apellidos%"])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Se han encontrado {$resultado->fetchColumn()} registro(s).</p>\n";
print "\n";
}
// CONSULTA DE SELECCIÓN DE REGISTROS QUE DEVUELVE UN ÚNICO REGISTRO DE UNA COLUMNA
$apellidos = "con"; // Normalmente este valor vendrá de un formulario
$consulta = "SELECT COUNT(*) FROM $cfg[tablaPersonas]
WHERE apellidos LIKE :apellidos";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":apellidos" => "$apellidos%"])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Se han encontrado {$resultado->fetchColumn()} registro(s).</p>\n";
print "\n";
}
Si el campo es de tipo numérico, y la cadena de búsqueda es una cadena, podríamos tener un problema de tipos al compararlos. En el caso de muchas bases de datos (como SQLite o MySQL) la conversión de tipos es automática y el valor numérico de la base de datos se convierte automáticamente en una cadena para permitir la búsqueda la cadena de búsqueda, pero en otras (como PostgreSQL) la conversión no se realiza y se produce un error.
Para escribir consultas que se puedan ejecutar sin problemas en el mayor número de sistemas gestores de bases de datos, es recomendable realizar la conversión usando la función SQL como muestra el ejemplo siguiente en el que "edad" sería un campo INTEGER:
$consulta = "SELECT * FROM $cfg[tablaPersonas]
WHERE nombre LIKE :nombre
AND CAST(edad AS VARCHAR) LIKE :edad
ORDER BY $ordena";
Se pueden también realizar consultas de unión entre varias tablas, como en el ejemplo siguiente. El ejemplo está sacado del ejercicio de Biblioteca y devuelve los préstamos pendientes de devolver a partir de tres tablas de Usuarios, Obras y Préstamos:
// CONSULTA DE UNIÓN DE TABLAS
$consulta = "SELECT
$cfg[tablaPrestamos].id AS id,
$cfg[tablaUsuarios].nombre as nombre,
$cfg[tablaUsuarios].apellidos as apellidos,
$cfg[tablaObras].titulo as titulo,
$cfg[tablaPrestamos].prestado as prestado,
$cfg[tablaPrestamos].devuelto as devuelto
FROM $cfg[tablaPrestamos], $cfg[tablaUsuarios], $cfg[tablaObras]
WHERE
$cfg[tablaPrestamos].id_usuario=$cfg[tablaUsuarios].id
AND $cfg[tablaPrestamos].id_obra=$cfg[tablaObras].id
AND $cfg[tablaPrestamos].devuelto='0000-00-00'
ORDER BY $columna $orden";
$resultado = $pdo->query($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
...
En los ejercicios en este curso se propone al alumno organizar los programas de manera que puedan trabajar tanto con SQLite como con MySQL y hacerlo de forma organizada, para que se puedan añadir fácilmente otras bases de datos.
Para ello distribuiremos los elementos comunes (constantes, variables y funciones) en cuatro ficheros distintos
Todas las páginas de la aplicación cargarán la biblioteca general, que a su vez cargará el fichero de configuración y una de las dos bibliotecas específicas, de acuerdo con la configuración escogida por el usuario.
Por ejemplo, para el caso de la función de conexión, el resultado sería:
// config.php
// VARIABLES CONFIGURABLES POR EL ADMINISTRADOR DE LA APLICACIÓN
// Base de datos utilizada por la aplicación
$cfg["dbMotor"] = SQLITE; // Valores posibles: MYSQL o SQLITE
// Configuración para SQLite
$cfg["sqliteDatabase"] = "/tmp/db.sqlite"; // Ubicación de la base de datos
// Configuración para MySQL
$cfg["mysqlHost"] = "localhost"; // Nombre de host
$cfg["mysqlUser"] = ""; // Nombre de usuario
$cfg["mysqlPassword"] = ""; // Contraseña de usuario
$cfg["mysqlDatabase"] = ""; // Nombre de la base de datos
// biblioteca.php
// CONSTANTES
define("SQLITE", 1);
define("MYSQL", 2);
require_once "config.php";
if ($cfg["dbMotor"] == SQLITE) {
require_once "biblioteca-sqlite.php";
} elseif ($cfg["dbMotor"] == MYSQL) {
require_once "biblioteca-mysql.php";
}
// biblioteca-sqlite.php
// SQLITE: Conexión con la base de datos
function conectaDb()
{
global $cfg;
try {
$tmp = new PDO("sqlite:$cfg[sqliteDatabase]");
$tmp->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$tmp->query("PRAGMA foreign_keys = ON");
$tmp->query("PRAGMA encoding = 'UTF-8'");
return $tmp;
} catch (PDOException $e) {
print " <p class=\"aviso\">Error: No puede conectarse con la base de datos. {$e->getMessage()}</p>\n";
exit;
}
}
// biblioteca-mysql.php
// MYSQL: Conexión con la base de datos
function conectaDb()
{
global $cfg;
try {
$tmp = new PDO("mysql:host=$cfg[mysqlHost];dbname=$cfg[mysqlDatabase];charset=utf8mb4", $cfg["mysqlUser"], $cfg["mysqlPassword"]);
} catch (PDOException $e) {
$tmp = new PDO("mysql:host=$cfg[mysqlHost];charset=utf8mb4", $cfg["mysqlUser"], $cfg["mysqlPassword"]);
} catch (PDOException $e) {
print " <p class=\"aviso\">Error: No puede conectarse con la base de datos. {$e->getMessage()}</p>\n";
exit;
} finally {
$tmp->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$tmp->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
return $tmp;
}
}
// EJEMPLO DE USO DE CONEXIÓN CONFIGURABLE
// La conexión se debe realizar en cada página que acceda a la base de datos
require_once "biblioteca.php";
$pdo = conectaDb();
El resultado podría ser el siguiente
// biblioteca.php
// Configuración de la tabla Personas
$cfg["tablaPersonas"] = "personas"; // Nombre de la tabla Personas
// config.php
// Configuración de la tabla Personas
$cfg["tablaPersonasTamNombre"] = 40; // Tamaño de la columna Personas > Nombre
$cfg["tablaPersonasTamApellidos"] = 60; // Tamaño de la columna Personas > Apellidos
// biblioteca-sqlite.php
// FUNCIONES ESPECÍFICAS DE LA BASE DE DATOS SQLITE
// SQLITE: Consultas de borrado y creación de tablas
function borraTodo()
{
global $pdo, $cfg;
$consulta = "DROP TABLE IF EXISTS $cfg[tablaPersonas]";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al borrar la tabla. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla borrada correctamente (si existía).</p>\n";
}
print "\n";
$consulta = "CREATE TABLE $cfg[tablaPersonas] (
id INTEGER PRIMARY KEY,
nombre VARCHAR($cfg[tablaPersonasTamNombre]),
apellidos VARCHAR($cfg[tablaPersonasTamApellidos])
)";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la tabla. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla creada correctamente.</p>\n";
}
print "\n";
}
// biblioteca-mysql.php
// FUNCIONES ESPECÍFICAS DE LA BASE DE DATOS MYSQL
// MYSQL: Consultas de borrado y creación de base de datos y tablas
function borraTodo()
{
global $pdo, $cfg;
$consulta = "DROP DATABASE IF EXISTS $cfg[mysqlDatabase]";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al borrar la base de datos. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Base de datos borrada correctamente (si existía).</p>\n";
}
print "\n";
$consulta = "CREATE DATABASE $cfg[mysqlDatabase]
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la base de datos. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Base de datos creada correctamente.</p>\n";
print "\n";
$consulta = "USE $cfg[mysqlDatabase]";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Base de datos seleccionada correctamente.</p>\n";
print "\n";
$consulta = "CREATE TABLE $cfg[tablaPersonas] (
id INTEGER UNSIGNED AUTO_INCREMENT,
nombre VARCHAR($cfg[tablaPersonasTamNombre]),
apellidos VARCHAR($cfg[tablaPersonasTamApellidos]),
PRIMARY KEY(id)
)";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la tabla. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla creada correctamente.</p>\n";
}
}
}
}
// EJEMPLO DE USO DE BORRADO CONFIGURABLE
require_once "biblioteca.php";
$pdo = conectaDb();
borraTodo();