Forma eficiente de gestionar errores al ejecutar sentencias con PDO

publicado por: Anonymous

Estoy trabajando en un proyecto con PHP y realizo este tipo de consultas:

$stmt = $conn->prepare($sql);
// Bindeo algunos parametros...
$stmt.execute();

Quiero saber cuál es la forma más eficiente de gestionar cualquier tipo de error que pueda llegar a ocurrir al hacer la consulta, ya sea esa que escribí, o un insert(), un executeUpdate(), etc. Ya que no sé si arrojan excepciones o simplemente alcanza con validar los valores de retorno.

¿Cuál forma es la más completa para gestionar estos errores?

if($stmt.execute()) 
    echo "Exito!";
else
    echo "Falló";

O atender excepciones:

try {
    $stmt.execute();
} catch(Exception $e) {
    print_r($e);
}

¿Combinar ambas? ¿Cambia algo si son operaciones de inserción o update?

solución

Al usar PDO hay que tener en cuenta dos aspectos muy importantes que atañen a la seguridad del manejo de los datos. Uno de ellos tiene que ver precisamente con el manejo de las excepciones.


I

Antes de ver esos dos aspectos, quiero decir primero que la mejor manera de usar PDO es tener una clase dedicada al manejo de nuestra conexión a PDO, la cual usaremos cada vez que necesitemos conectarnos a la base de datos. Eso nos evita tener cada vez que crear el objeto PDO, configurarlo adecuadamente, etc, etc. Teniendo una clase simplemente obtenemos nuestro objeto mediante la palabra clave new y listo. Además, podemos dotar a esa clase de métodos personalizados según nuestras necesidades, el binding y otras cosas se podrían simplificar de una forma interesante, dando muchísima facilidad a nuestro código.

Si se quiere, se puede echar una mirada a esta Clase que estoy creando, y que funciona ahora perfectamente para un uso básico de PDO.

Veamos entonces esos dos aspectos mencionados anteriormente:

1. El manejo de las excepciones y un posible peligro

Cuando creamos la conexión a PDO, si no se pasan los parámetros adecuados al momento de crear el objeto de conexión, PDO podría revelar las credenciales de conexión (nombre de usuario, contraseña, nombre de la base de datos…) en el error_log en caso de ocurrir un error. El registro de errores, no está de más decirlo, es uno de los archivos preferidos de cualquier hacker, precisamente porque en él se pueden encontrar datos interesantes para cualquier usuario mal intencionado.

¿Cómo evitarlo?

Estableciendo el siguiente parámetro en la conexión: PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION. Para más detalles ver, en el Manual de PHP: Errores y su manejo

Este sería entonces unos de los atributos que habría que pasar en opción a la conexión PDO.

Veámoslo aquí, este sería el método de conexión de una Clase dedicada a gestionar nuestra conexión PDO. Este es el método de la clase mencionada más arriba:

    private function Connect()
    {
    /* Leer credenciales desde el  archivo ini */
        $this->credenciales = parse_ini_file("//home3/deiverbu/.credentials/db.php.ini");
        $dsn = 'mysql:dbname=' . $this->credenciales["dbnombre"] . ';host=' . $this->credenciales["host"] . '';
        $pwd = $this->credenciales["clave"];
        $usr = $this->credenciales["usuario"];

    /**
     *  El array $options es muy importante para tener un PDO bien configurado
     *  
     *  1. PDO::ATTR_PERSISTENT => true: sirve para usar conexiones persistentes
     *      se puede establecer a false si no se quiere usar este tipo de conexión. Ver: https://es.stackoverflow.com/a/50097/29967 
     *  2. PDO::ATTR_EMULATE_PREPARES => false: Se usa para desactivar emulación de consultas preparadas 
     *      forzando el uso real de consultas preparadas. 
     *      Es muy importante establecerlo a false para prevenir Inyección SQL. Ver: https://es.stackoverflow.com/a/53280/29967
     *  3. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION También muy importante para un correcto manejo de las excepciones. 
     *      Si no se usa bien, cuando hay algún error este se podría escribir en el log revelando datos como la contraseña !!!
     *  4. PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'": establece el juego de caracteres a utf8, 
     *      evitando caracteres extraños en pantalla. Ver: https://es.stackoverflow.com/a/59510/29967
     */
        $options = array(
            PDO::ATTR_PERSISTENT => true, 
            PDO::ATTR_EMULATE_PREPARES => false, 
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'"
        );
        try {
            # Intentar la conexión 
            $this->pdo = new PDO($dsn, $usr, $pwd, $options);

            # Conexión exitosa, asignar true a la variable booleana isConnected.
            $this->isConnected = true;
        }
        catch (PDOException $e) {
            # Escribir posibles excepciones en el error_log
        error_log($this->error = $e->getMessage(),0);
    }
    }

2. El problema de las preparaciones emuladas

El segundo aspecto importante tiene que ver con las consultas preparadas y las preparaciones emuladas.

Sobre PDO::ATTR_EMULATE_PREPARES el Manual dice lo siguiente:

Habilita o deshabilita la emulación de sentencias preparadas. Algunos
controladores no admiten instrucciones preparadas nativas o tienen un
soporte limitado para ellas. Utilice esta configuración para obligar a
PDO a emular siempre instrucciones preparadas (si es VERDADERO), o
intentar utilizar instrucciones preparadas nativas (si FALSO). Siempre
volverá a emular la instrucción preparada si el controlador no puede
preparar con éxito la consulta actual.

¿Cuál es el problema?

Que en PDO esta opción viene establecida a truepor defecto, y si no la establecemos a false al momento de crear el objeto conexión, podríamos tener una Inyección SQL preparada inteligentemente por un usario mal intencionado.

La forma en que esto podría ocurrir, esta explicado con todo detalle y ejemplos en mi respuesta a la pregunta: ¿Cómo evitar la inyección SQL en PHP?.

Por eso, verán que en el código de conexión de más arriba, tenemos esto:

PDO::ATTR_EMULATE_PREPARES => false, 

Si esto no está, sencillamente nuestro objeto conexión no es del todo seguro y podríamos llevarnos tarde o temprano la sorpresa de una Inyección SQL aunque estemos usando PDO.


II

Pasemos ahora a una segunda parte que respondería más directamente a tu pregunta.

Si en nuestra clase de conexión tenemos un bloque try ... catch como el mostrado más arriba, podemos olvidarnos de seguir usándolo caaaada vez que tengamos que hacer una consulta a la base de datos. De eso se encargará la clase, que para eso existe. Nosotros tendremos que ocuparnos solamente de crear nuestro objeto conexión mediante new, hacer las consultas llamando a los métodos de la clase y manejar los datos.

Algunos usos básicos, usando nuestro objeto conexión

Un SELECT sin criterios

Se haría sencillamente así:

$datos = $mipdo->query("SELECT * FROM padres");

Y para verificar si hay datos o no en el SELECT

if ($datos) 
{
    echo "Hay datos";
}
else
{
    echo "NO hay datos";    
}

Un SELECT con criterios

$id_provincia=1;
$id_estado=5;
$sql= "SELECT * FROM tabla WHERE id_provincia = :id_provincia AND id_estado = :id_estado;";

$mipdo->bind("id_provincia", $id_provincia);
$mipdo->bind("id_estado",$id_estado);
$datos=$mipdo->query($sql);

Y para saber si hay datos, verificamos, como más arriba, nuestra variable $datos.

Un UPDATE cualquiera

$id_provincia=1;
$id_estado=5;

$sql= "UPDATE tabla SET id_provincia=:id_provincia WHERE id_estado=:id_estado;";
$mipdo->bind("id_provincia", $id_provincia);
$mipdo->bind("id_estado",$id_estado);
$datos=$mipdo->query($sql);

Para verificar si se actualizaron registros, sólo tenemos que evaluar la variable datos, como hemos hecho antes, con la diferencia de que en el caso de UPDATE, en caso de haber actualizaciones, la variable contendrá el total de ellas. Entonces:

if ($datos) 
{
    echo "Se actualizaron ".$datos." registros";
}
else
{
    echo "NO se actualizaron registros";    
}

Un INSERT cualquiera

$id_provincia=2;
$id_estado=6;

$sql= "INSERT INTO tabla (id_provincia, id_estado) VALUES (:id_provincia, :id_estado;";
$mipdo->bind("id_provincia", $id_provincia);
$mipdo->bind("id_estado",$id_estado);
$datos=$mipdo->query($sql);

Para verificar si se insertaron registros, sólo tenemos que evaluar la variable datos, como hemos hecho antes en el caso de UPDATE. Entonces:

if ($datos) 
{
    echo "Se insertaron ".$datos." registros";
}
else
{
    echo "NO se insertaron registros";    
}

Conclusión

Como se puede apreciar, el uso de una clase dedicada simplifica cualquier posterior manejo de los datos. Y la clase se quiere personalizar si se desean hacer otras verificaciones, según las necesidades del programador.

Respondido por: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *