Diferencia entre revisiones de «PHP UD3 OP BAIXA»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 4 ediciones intermedias del mismo usuario)
Línea 1: Línea 1:
−
===Introducción===
+
=== Introdución ===
  
 +
Nesta unidade veremos como realizar operacións de baixa sobre unha base de datos MySQL/MariaDB desde PHP 8.5.
  
−
* '''<u>Requerimentos previos:</u>'''  
+
O mecanismo principal empregado nos apuntes será '''PDO (PHP Data Objects)'''. Ao final inclúese tamén unha explicación equivalente empregando '''mysqli''' como ampliación.
−
:* Ter creada a base de datos indicada [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql nunha sección anterior]
+
 
−
:* Ter gardados os parámetros de conexión nun arquivo separado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro como foi indicado nunha sección anterior]
+
* '''<u>Requirimentos previos:</u>'''
−
:* Estes exercicios están baseados nunha versión de PHP 8.1 ou superior onde as ordes PHP para manexar a base de datos lanzan expcecións. Se tedes unha vesión anterior, teredes que facer o indicado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_CONSULTA na sección de consultas] e na [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Control_de_errores sección de conexión].
+
:* Ter creada a base de datos indicada [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql nunha sección anterior].
 +
:* Ter gardados os parámetros de conexión nun arquivo separado, fóra da ruta <code>/var/www/html</code>.
 +
:* Ter instalada e habilitada a extensión <code>pdo_mysql</code>.
 +
:* Os exemplos están pensados para '''PHP 8.5'''.
 +
:* Partiremos do código realizado anteriormente na sección de [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA altas].
 +
 
 +
* '''NOTA IMPORTANTE:''' nesta unidade realizaremos os borrados mediante peticións <code>POST</code>. Non empregaremos unha ligazón <code>GET</code> para executar directamente unha operación que modifica datos.
 +
 
 +
: A protección fronte a ataques '''CSRF (Cross-Site Request Forgery)''' incorporarase cando se estuden as sesións. Unha petición <code>POST</code> por si soa non evita un ataque CSRF.
 +
 
 +
 
 +
* '''CONSIDERACIÓNS:'''
 +
:* Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos)
 +
:* Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro vimos nun punto anterior desta sección].
 +
:* Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas.
 +
:* En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes.
 +
:* Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...).
 +
:* Lembrar que tamén podedes activar o debugger como está indicado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Habilitar_depuraci%C3%B3n_en_VS_Code nesta sección da wiki].
  
  
−
* '''NOTA IMPORTANTE:''' [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios Vimos en seccións anteriores] que cando empregmos datos dun formulario, existen unhas técnicas para evitar posibles ataques. Todos os exercicios que veñen a continuación empregan ditas técnicas. Existe un tipo de ataque que non se ten en conta ao non ter chegado á [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Sesi%C3%B3ns#Ataques_CSRF_.28Cross-Site_Request_Forgery.29 sección de emprego de variables de sesións]. En dita sección se explica o que temos que facer. É importante que vos acordedes de aplicar a técnica descrita cando vexades os contidos.
 
  
  
Línea 14: Línea 31:
  
 
<br />
 
<br />
−
* O proceso de baixa faise por medio da [http://www.w3schools.com/sql/sql_delete.asp orde sql DELETE].
 
  
−
* Podemos eliminar unha fila ou un conxunto de filas en base a uns criterios (parte WHERE da cláusula DELETE)
+
==== Organización dos exemplos ====
 +
 
 +
No arquivo '''/var/www/confBD/bd.php''', situado fóra da ruta pública <code>/var/www/html</code>, gardaremos os datos da conexión e as consultas:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
 
 +
define('SERVIDOR', 'localhost');
 +
define('USUARIO', 'user_php');
 +
define('PASSWORD', 'user_php');
 +
define('BD', 'php');
 +
 
 +
define(
 +
    'NOME_DA_CONSULTA',
 +
    'SELECT col1, col2
 +
    FROM taboa
 +
    ORDER BY col1'
 +
);
 +
</syntaxhighlight>
 +
 
 +
Nas páxinas PHP que accedan á base de datos empregaremos como base o seguinte esquema:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
 
 +
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
 +
 
 +
    // Operacións coa base de datos...
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
 +
 
 +
    die('Erro relacionado coa base de datos.');
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    error_log($e->getMessage());
  
−
* Ao igual que fixemos anteriormente, podemos borrar directamente en PHP facendo uso da orde DELETE ou chamar  a un procedemento almacenado, enviando os datos necesarios e executando a sentenza.
+
    die('Produciuse un erro na aplicación.');
 +
}
 +
</syntaxhighlight>
  
 +
<br />
  
−
* '''IMPORTANTE:''' Temos que ter en conta que nas bases de datos relacionais, dar de baixa un rexistro pode implicar borrar rexistros relacionados.
+
=== Baixa de rexistros: DELETE ===
−
: Cando creamos as táboas indicamos as relacións entre elas por medio de claves foráneas, nas que indicamos como queremos que se comporten cando se produce un borrado ou unha modificación. Máis información [https://dev.mysql.com/doc/refman/5.7/en/create-table-foreign-keys.html neste enlace].
 
  
 +
* O proceso de baixa realízase mediante a orde SQL <code>DELETE</code>.
  
−
: Por exemplo, na táboa LIBROS_AUTORES:
+
Por exemplo:
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="6,7" >
 
−
CREATE TABLE `LIBROS_AUTORES` (
 
−
  `libro_id` int(11) NOT NULL,
 
−
  `autor_id` int(11) NOT NULL,
 
−
  PRIMARY KEY (`libro_id`,`autor_id`),
 
−
  KEY `fk_LIBROS_AUTORES_AUTORES_idx` (`autor_id`),
 
−
  CONSTRAINT `fk_LIBROS_AUTORES_AUTORES` FOREIGN KEY (`autor_id`) REFERENCES `AUTORES` (`id_autor`) ON DELETE NO ACTION ON UPDATE CASCADE,
 
−
  CONSTRAINT `fk_LIBROS_AUTORES_LIBROS` FOREIGN KEY (`libro_id`) REFERENCES `LIBROS` (`id_libro`) ON DELETE NO ACTION ON UPDATE CASCADE
 
−
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish2_ci;
 
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
DELETE FROM estados
 +
WHERE id = 10;
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: No exemplo estamos a indicar que en caso de borrar unha fila de táboa AUTORES, non se vai permitir se existen filas na táboa LIBROS_AUTORES.  
+
* Podemos eliminar unha fila ou un conxunto de filas empregando a condición indicada na cláusula <code>WHERE</code>.
−
: Se actualizamos a clave primaria da táboa AUTORES, actualizará a clave na táboa AUTORES_LIBROS (parte ON UPDATE CASCADE).
+
 
 +
* '''IMPORTANTE:''' debemos ter especial coidado coa cláusula <code>WHERE</code>. A seguinte orde eliminaría '''todas as filas''' da táboa:
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
DELETE FROM estados;
 +
</syntaxhighlight>
  
−
* Polo tanto, será tarefa do programador comprobar se os datos están sendo utilizados noutras táboas e avisar en caso de que o usuario intente borralos ou ben permitir borrados en cascada ou ben capturar o erro que dará o mysql en caso de intentar borrar datos relacionados e informar ao usuario do problema.
+
* Desde PHP, sempre que o criterio de borrado conteña datos variables, empregaremos unha '''consulta preparada''':
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'DELETE FROM estados
 +
    WHERE id = :id_estado'
 +
);
  
−
=== Caso Práctico: Baixa ===
+
$stmt->execute([
 +
    ':id_estado' => $id
 +
]);
 +
</syntaxhighlight>
  
 +
Non construiremos a orde SQL introducindo directamente o identificador mediante concatenación ou <code>sprintf()</code>.
  
 
<br />
 
<br />
−
====Orientado a procedementos====
+
 
−
* Imos implementar esta opción na páxina '''consultar_estados.php''' continuando có visto [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA na sección de ALTA].
+
=== Borrado e integridade referencial ===
−
: Se estamos a utilizar procedementos almacenados, lembrar que en caso de erro '2014 - Commands out of sync; you can't run this command now', debemos engadir a seguinte liña despois de chamar a un procedemento almacenado:
+
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="9" >
+
Nas bases de datos relacionais un rexistro pode estar relacionado con rexistros doutras táboas mediante claves foráneas.
−
                    if ($conex->multi_query($proc)){
+
 
−
                        $result = $conex->store_result();
+
Ao definir unha clave foránea podemos indicar que debe ocorrer cando se elimina ou modifica a fila referenciada.
−
                        if ($result && ($result->num_rows>0)){
+
 
−
                            while($fila=$result->fetch_assoc()){
+
As opcións máis habituais son:
−
                                // Procesamos cada fila
+
 
−
                            }
+
:* <code>RESTRICT</code>: impide o borrado se existen filas relacionadas.
−
                            $result->free();
+
:* <code>NO ACTION</code>: en MySQL ten un comportamento equivalente a <code>RESTRICT</code> para este propósito.
−
                        }
+
:* <code>CASCADE</code>: elimina automaticamente as filas dependentes.
−
                        while($conex->next_result() && $conex->store_result());
+
:* <code>SET NULL</code>: establece a clave foránea a <code>NULL</code>, sempre que a columna o permita.
−
                    }
+
 
 +
Por exemplo:
 +
 
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
CREATE TABLE LIBROS_AUTORES (
 +
    libro_id INT NOT NULL,
 +
    autor_id INT NOT NULL,
 +
 
 +
    PRIMARY KEY (libro_id, autor_id),
 +
 
 +
    CONSTRAINT fk_libros_autores_autores
 +
        FOREIGN KEY (autor_id)
 +
        REFERENCES AUTORES (id_autor)
 +
        ON DELETE RESTRICT
 +
        ON UPDATE CASCADE,
 +
 
 +
    CONSTRAINT fk_libros_autores_libros
 +
        FOREIGN KEY (libro_id)
 +
        REFERENCES LIBROS (id_libro)
 +
        ON DELETE RESTRICT
 +
        ON UPDATE CASCADE
 +
) ENGINE=InnoDB
 +
  DEFAULT CHARSET=utf8mb4;
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Neste exemplo:
 +
 +
:* Non se poderá eliminar un autor se existen filas de <code>LIBROS_AUTORES</code> que fan referencia a ese autor.
 +
:* Se se modifica a clave referenciada e a relación está definida con <code>ON UPDATE CASCADE</code>, a modificación propágase ás filas relacionadas.
  
 +
Polo tanto, ao deseñar unha aplicación debemos decidir se:
  
−
* O que temos que facer é:
+
:* impedimos o borrado e informamos ao usuario;
−
:* Definir no arquivo bd.php unha constante coa orde SQL para dar de baixa a un estado.
+
:* eliminamos previamente os datos dependentes;
−
:* Modificar a páxina consultar_estados.php e engadir a opción 'Eliminar' a cada un dos estados. Debemos ir a unha páxina procesar_borrar_estado.php.
+
:* empregamos <code>ON DELETE CASCADE</code>;
−
:: O poderemos facer:
+
:* ou aplicamos outra regra de integridade coherente co modelo de datos.
−
:* Cun enlace href, enviando como parámetro o id do estado a borrar.
 
−
:* Cun formulario e un campo oculto có id do estado a borrar. Eu vou escoller esta opción, pero sodes libres de practicar e intentalo coa primimeira.
 
−
:* Crear a páxina procesar_borrar_estado.php que dará de baixa o estado e se se produce algún erro deberá de informar á páxina de consultar_estados.php de cal foi o erro. Ao igual que na alta, poderemos enviar dita información nun parámetro na URL de redirección ou veremos nas seguintes seccións como empregar variables de sesión.
 
−
:: A xestión do erro en caso de querer borrar un rexistro relacionado a imos facer na páxina PHP comprobando que o valor numérico do erro sexa o 1451, que se corresponde co erro 'Cannot delete or update a parent row: a foreign key constraint fails'.
 
−
:: Lembrar que dito control poderíamos facelo no propio procedemento almacenado capturando a excepción ou comprobando cun select previo se existe o rexistro relacionado.
 
  
 +
A decisión forma parte tamén do deseño da base de datos, non só do código PHP.
  
 +
<br />
  
−
* <u>Nota:</u>
+
=== Caso práctico: baixa dun estado ===
−
:* Lembrar que nesta solución estamos a utilizar a chamada a '''multi_query''' para obter o número de rexistros borrados, pero se podería utilizar a chamada a '''query''' no caso de que o procedemento non devolva resultados ou non nos interese obter o número de rexistros borrados.
 
−
:*: Nesta solución non comprobamos se xa existen datos relacionados con ese estado antes de borralos.
 
−
:* Usaremos como base o código feito na solución '''consultar_estados.php''' [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA feito anteriormente na sección de alta].
 
  
 +
Imos continuar o mantemento da táboa <code>ESTADOS</code> realizado na sección de altas.
  
 +
O obxectivo será:
  
 +
:* Amosar os estados existentes.
 +
:* Engadir unha opción '''Borrar''' en cada fila.
 +
:* Enviar o identificador mediante un formulario <code>POST</code>.
 +
:* Validar no servidor que o identificador recibido é correcto.
 +
:* Executar un <code>DELETE</code> mediante unha consulta preparada.
 +
:* Informar do número de rexistros eliminados.
 +
:* Tratar o caso no que a fila non pode eliminarse debido a unha clave foránea.
  
 +
<br />
  
 +
==== Arquivo /var/www/confBD/bd.php ====
  
−
<br />
+
Engadimos a seguinte consulta ao arquivo empregado nas seccións anteriores:
−
'''Posible solución Caso Práctico: Baixa'''
 
  
−
'''Arquivo bd.php'''. Engadimos o seguinte ao que xa tiñamos.
+
::<syntaxhighlight lang="php" enclose="div">
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
define(
−
define('BORRAR_ESTADO','DELETE FROM estados where id=%d');
+
    'BORRAR_ESTADO',
 +
    'DELETE FROM estados
 +
    WHERE id = :id_estado'
 +
);
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Fixarse en que o identificador non se introduce directamente na cadea SQL. Empregamos o parámetro <code>:id_estado</code>.
 +
 +
<br />
 +
 +
==== Modificación de consultar_estados.php ====
  
 +
Partimos da páxina creada na sección de alta e engadimos unha columna de accións.
  
−
'''Arquivo consultar_estados.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
:::<syntaxhighlight lang="java" enclose="div" highlight="39-42,56" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
 +
 
 +
    $stmt = $conex->query(CONSULTAR_ESTADOS);
 +
    $estados = $stmt->fetchAll();
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
  
−
try {
+
     die('Erro relacionado coa base de datos.');
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  
+
 
−
     $conex->set_charset('utf8mb4');
+
} catch (Throwable $e) {
 +
 
 +
     error_log($e->getMessage());
  
 +
    die('Produciuse un erro na aplicación.');
 +
}
 
?>
 
?>
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<!DOCTYPE html>
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
<html lang="gl">
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
<head>
−
    <head>
+
    <meta charset="UTF-8">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
    <title>Xestión de estados</title>
−
        <title>Xestión de estados</title>
+
</head>
−
    </head>
+
<body>
−
     <body>
+
 
−
        <form action="alta_estado.php" method="post">
+
     <h1>Xestión de estados</h1>
−
            <button type="submit">Novo Estado</button>
+
 
−
        </form>
+
    <form action="alta_estado.php" method="get">
−
         <table border="1" style="margin-top:20px">
+
        <button type="submit">Novo estado</button>
 +
    </form>
 +
 
 +
    <table>
 +
         <thead>
 
             <tr>
 
             <tr>
−
                 <th>Estado</th>
+
                 <th>Id</th>
 +
                <th>Descrición</th>
 
                 <th>Accións</th>
 
                 <th>Accións</th>
 
             </tr>
 
             </tr>
−
         <?php
+
        </thead>
 +
 
 +
        <tbody>
 +
 
 +
         <?php foreach ($estados as $estado): ?>
 +
 
 +
            <tr>
 +
                <td>
 +
                    <?= htmlspecialchars(
 +
                        (string) $estado['id'],
 +
                        ENT_QUOTES | ENT_SUBSTITUTE,
 +
                        'UTF-8'
 +
                    ) ?>
 +
                </td>
 +
 
 +
                <td>
 +
                    <?= htmlspecialchars(
 +
                        $estado['descripcion'],
 +
                        ENT_QUOTES | ENT_SUBSTITUTE,
 +
                        'UTF-8'
 +
                    ) ?>
 +
                </td>
 +
 
 +
                <td>
 +
                    <form
 +
                        action="procesar_borrar_estado.php"
 +
                        method="post"
 +
                        onsubmit="return confirm(
 +
                            'Estás seguro de que queres borrar este estado?'
 +
                        );"
 +
                    >
 +
                        <input
 +
                            type="hidden"
 +
                            name="id"
 +
                            value="<?= htmlspecialchars(
 +
                                (string) $estado['id'],
 +
                                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                                'UTF-8'
 +
                            ) ?>"
 +
                        >
 +
 
 +
                        <button
 +
                            type="submit"
 +
                            name="btnBorrarEstado"
 +
                            value="BORRAR"
 +
                        >
 +
                            Borrar
 +
                        </button>
 +
                    </form>
 +
                </td>
 +
            </tr>
  
−
            $resultado = $conex->query(CONSULTAR_ESTADOS);
+
        <?php endforeach; ?>
  
−
            while($fila = $resultado->fetch_assoc()) {
+
        </tbody>
−
                $nombre = htmlentities($fila['descripcion']);
+
    </table>
−
                echo "<tr>";
 
−
                echo "<td>$nombre</td>";
 
−
                echo "<td>";
 
−
                printf("<form action='procesar_borrar_estado.php' method='post'>");
 
−
                printf("<input type='hidden' name='id' value='%s' />",htmlentities($fila['id']));
 
−
                echo '<input onclick="return confirm(\'Estás seguro de que queres borrar este estado?\');" type="submit" value="Borrar" name="btnBorrarEstado" style="background:none;border:none;padding:0;maring:0;color: blue; text-decoration:underline; cursor:pointer; font-size:inherit" />';
 
−
                echo '</form>';
 
−
                echo "</td>";
 
  
−
                 echo "</tr>";
+
    <?php if (isset($_GET['id'])): ?>
−
            }
+
        <p>
 +
            Estado con id
 +
            <?= htmlspecialchars(
 +
                $_GET['id'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                 'UTF-8'
 +
            ) ?>
 +
            dado de alta correctamente.
 +
        </p>
 +
    <?php endif; ?>
  
−
             $resultado->free();
+
    <?php if (isset($_GET['msg'])): ?>
−
        ?>  
+
        <p>
−
         </table>
+
             <?= htmlspecialchars(
 +
                $_GET['msg'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            ) ?>
 +
         </p>
 +
    <?php endif; ?>
  
 +
    <?php if (isset($_GET['error'])): ?>
 +
        <p>
 +
            <?= htmlspecialchars(
 +
                $_GET['error'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            ) ?>
 +
        </p>
 +
    <?php endif; ?>
  
−
        <div>
+
</body>
−
            <?php
 
−
                if(isset($_GET['id'])) printf("<h3>Estado con id %s dado de alta correctamente.</h3>",htmlentities($_GET['id']));
 
−
                if(isset($_GET['msg'])) printf("<h4>%s</h3>",htmlentities($_GET['msg']));
 
−
                if(isset($_GET['error'])) printf("<h2>Houbo un erro: %s</h2>",htmlentities($_GET['error']));
 
−
            ?>
 
−
        </div>   
 
−
       
 
−
    </body>  
 
 
</html>
 
</html>
 +
</syntaxhighlight>
 +
 +
Neste exemplo empregamos un formulario <code>POST</code> para a operación de borrado.
 +
 +
Non empregaremos unha URL do tipo:
 +
 +
::<syntaxhighlight lang="html" enclose="div">
 +
<a href="procesar_borrar_estado.php?id=5">
 +
    Borrar
 +
</a>
 +
</syntaxhighlight>
 +
 +
porque unha petición <code>GET</code> non debería empregarse para provocar directamente unha modificación dos datos.
 +
 +
A chamada JavaScript a <code>confirm()</code> evita borrados accidentais por parte do usuario, pero '''non constitúe unha medida de seguridade'''. A validación e autorización deben realizarse sempre no servidor.
 +
 +
<br />
 +
 +
==== Arquivo procesar_borrar_estado.php ====
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
−
}   // Pecha o try da parte superior da páxina
+
 
−
catch (mysqli_sql_exception $e) {
+
try {
−
     die("Erro mysql: " . $e->getMessage());
+
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
 +
 
 +
    if (
 +
        !isset($_POST['btnBorrarEstado']) ||
 +
        $_POST['btnBorrarEstado'] !== 'BORRAR'
 +
    ) {
 +
        throw new RuntimeException(
 +
            'Operación non válida.'
 +
        );
 +
    }
 +
 
 +
    /*
 +
    * Un campo hidden tamén pode ser modificado polo usuario,
 +
    * polo que debemos validar o identificador no servidor.
 +
    */
 +
    $id = filter_input(
 +
        INPUT_POST,
 +
        'id',
 +
        FILTER_VALIDATE_INT,
 +
        [
 +
            'options' => [
 +
                'min_range' => 1
 +
            ]
 +
        ]
 +
    );
 +
 
 +
    if ($id === false || $id === null) {
 +
 
 +
        header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode('O identificador non é válido.')
 +
        );
 +
        exit;
 +
    }
 +
 
 +
    $stmt = $conex->prepare(BORRAR_ESTADO);
 +
 
 +
    $stmt->execute([
 +
        ':id_estado' => $id
 +
    ]);
 +
 
 +
    $numRexistros = $stmt->rowCount();
 +
 
 +
    if ($numRexistros === 1) {
 +
 
 +
        $mensaxe = 'Rexistro borrado correctamente.';
 +
 
 +
    } else {
 +
 
 +
        /*
 +
        * Non se produciu un erro SQL:
 +
        * simplemente non existía unha fila co id indicado.
 +
        */
 +
        $mensaxe =
 +
            'Non se atopou o rexistro que se quería borrar.';
 +
    }
 +
 
 +
    header(
 +
        'Location: consultar_estados.php?msg=' .
 +
        urlencode($mensaxe)
 +
    );
 +
    exit;
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
     /*
 +
    * getCode() devolve o SQLSTATE.
 +
    * errorInfo[1] contén o código específico do driver.
 +
    */
 +
    $sqlState = $e->getCode();
 +
    $codigoMySQL = $e->errorInfo[1] ?? null;
 +
 
 +
    error_log($e->getMessage());
 +
 
 +
    if (
 +
        $sqlState === '23000' &&
 +
        $codigoMySQL === 1451  // Podemos saber qué código é se imos a mysql e intentamos executar a mesma orde SQL Delete
 +
    ) {
 +
 
 +
        $mensaxe =
 +
            'Este rexistro está relacionado con outros datos ' .
 +
            'e non se pode borrar.';
 +
 
 +
    } else {
 +
 
 +
        $mensaxe =
 +
            'Erro relacionado coa base de datos.';
 +
    }
 +
 
 +
    header(
 +
        'Location: consultar_estados.php?error=' .
 +
        urlencode($mensaxe)
 +
    );
 +
    exit;
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    error_log($e->getMessage());
 +
 
 +
    header(
 +
        'Location: consultar_estados.php?error=' .
 +
        urlencode('Produciuse un erro na aplicación.')
 +
    );
 +
    exit;
 
}
 
}
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
 +
 +
==== Validación do identificador ====
 +
 +
O valor dun campo oculto dun formulario '''non é un dato de confianza'''.
 +
 +
Un usuario pode modificar manualmente a petición HTTP, polo que non debemos dar por feito que <code>$_POST['id']</code> contén necesariamente un número enteiro válido.
 +
 +
Por iso empregamos:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$id = filter_input(
 +
    INPUT_POST,
 +
    'id',
 +
    FILTER_VALIDATE_INT,
 +
    [
 +
        'options' => [
 +
            'min_range' => 1
 +
        ]
 +
    ]
 +
);
 +
</syntaxhighlight>
 +
 +
Desta maneira comprobamos que o identificador recibido é un enteiro maior ou igual que <code>1</code>.
 +
 +
'''IMPORTANTE:''' validar que o identificador sexa correcto non implica que o usuario teña permiso para borrar ese rexistro. Nunha aplicación real tamén será necesario comprobar a '''autorización'''.
 +
 +
<br />
 +
 +
==== Número de rexistros borrados ====
 +
 +
Despois de executar:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt->execute([
 +
    ':id_estado' => $id
 +
]);
 +
</syntaxhighlight>
 +
 +
podemos obter o número de filas afectadas mediante:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$numRexistros = $stmt->rowCount();
 +
</syntaxhighlight>
 +
 +
Para unha operación <code>DELETE</code> sobre un identificador único:
  
 +
:* <code>1</code>: existía a fila e foi eliminada.
 +
:* <code>0</code>: non existía ningunha fila que cumprise a condición.
  
 +
Que <code>rowCount()</code> devolva <code>0</code> non implica necesariamente un erro.
  
 
<br />
 
<br />
−
'''Arquivo procesar_borrar_estado.php'''
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="19" >
 
−
<?php
 
  
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
=== Tratamento dos erros de integridade referencial ===
−
ini_set('display_errors', 1);
+
 
−
ini_set('display_startup_errors', 1);
+
Se intentamos borrar unha fila que segue referenciada por unha clave foránea definida con <code>RESTRICT</code> ou <code>NO ACTION</code>, MySQL impedirá o borrado.
−
error_reporting(E_ALL);
+
 
 +
PDO lanzará unha <code>PDOException</code> porque configuramos:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
</syntaxhighlight>
 +
 
 +
Nunha <code>PDOException</code> podemos consultar:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$e->getCode()
 +
</syntaxhighlight>
 +
 
 +
que devolve o código '''SQLSTATE'''.
 +
 
 +
Para unha violación dunha restrición de integridade o SQLSTATE é:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
23000
 +
</syntaxhighlight>
 +
 
 +
Porén, <code>23000</code> é unha categoría xenérica de erros de integridade.
  
−
require ('/var/www/confBD/bd.php');
+
Se queremos saber se MySQL está indicando especificamente que unha fila pai segue referenciada, podemos consultar tamén:
  
−
$msg='';
+
::<syntaxhighlight lang="php" enclose="div">
−
$error = '';
+
$codigoMySQL = $e->errorInfo[1] ?? null;
−
if (isset($_POST['btnBorrarEstado'])) {
+
</syntaxhighlight>
−
  try {
 
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
 
−
    $conex->set_charset('utf8mb4');
 
  
−
    // Lóxica para dar de alta unha nova pista
+
e comprobar:
−
    $id = $conex->real_escape_string($_POST['id']);
 
−
    $consulta = sprintf(BORRAR_ESTADO,$id);
 
−
    if ($conex->multi_query($consulta)){
 
−
      $msg = sprintf("msg=%s%d", urlencode("Num. rexistros borrados:"),$conex->affected_rows);
 
−
      while($conex->next_result() && $conex->store_result());
 
−
    }
 
  
−
  }  // Pecha o try da parte superior da páxina. Lembrar que aos usuarios non se lle deben amosar os erros. Isto está posto a nivel informativo para os programadores
+
::<syntaxhighlight lang="php" enclose="div">
−
  catch (mysqli_sql_exception $e) {
+
if (
−
      $error=sprintf("error='Erro mysql: %s'",urlencode($e->getMessage()));
+
    $e->getCode() === '23000' &&
−
  }
+
    $codigoMySQL === 1451
−
  catch (Expcetion $e) {
+
) {
−
      $error=sprintf("error='Erro xeral: %s'",urlencode($e->getMessage()));
+
     // A fila segue referenciada por unha clave foránea.
−
  }
 
−
  finally{
 
−
      if(!empty($conex)){
 
−
          $conex->close();
 
−
      }
 
−
  }
 
−
 
 
−
      
 
 
}
 
}
−
$url = sprintf("consultar_estados.php?%s&%s",$msg,$error);
+
</syntaxhighlight>
−
header("Location:" . $url);
+
 
 +
O código MySQL <code>1451</code> corresponde ao erro:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
Cannot delete or update a parent row:
 +
a foreign key constraint fails
 +
</syntaxhighlight>
  
 +
'''IMPORTANTE:''' non debemos enviar directamente ao usuario:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$e->getMessage()
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
porque pode revelar nomes de táboas, columnas ou outros detalles internos.
  
−
====Empregando PDO====
+
Rexistraremos a información técnica:
−
* Imos implementar esta opción na páxina '''consultar_estados.php''' continuando có visto [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA na sección de ALTA].
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
error_log($e->getMessage());
 +
</syntaxhighlight>
  
−
* O que temos que facer é:
+
e amosaremos ao usuario unha mensaxe comprensible.
−
:* Definir no arquivo bd.php unha constante coa orde SQL para dar de baixa a un estado.
 
−
:* Modificar a páxina consultar_estados.php e engadir a opción 'Eliminar' a cada un dos estados. Debemos ir a unha páxina procesar_borrar_estado.php.
 
−
:: O poderemos facer:
 
−
:* Cun enlace href, enviando como parámetro o id do estado a borrar.
 
−
:* Cun formulario e un campo oculto có id do estado a borrar. Eu vou escoller esta opción, xa que é un pouco máis seguro que envialo pola URL.
 
−
:* Crear a páxina procesar_borrar_estado.php que dará de baixa o estado e se se produce algún erro deberá de informar á páxina de consultar_estados.php de cal foi o erro. Ao igual que na alta, poderemos enviar dita información nun parámetro na URL de redirección ou veremos nas seguintes seccións como empregar variables de sesión.
 
−
:: A xestión do erro en caso de querer borrar un rexistro relacionado a imos facer na páxina PHP comprobando que o valor numérico do erro sexa o 1217 o 1451, que se corresponde co erro 'Cannot delete or update a parent row: a foreign key constraint fails'. Podemos verificar estos códigos que son específicos de Mysql o comprobar si $e->getCode() = 23000 que se corresponde a violación de la integridad referencial.
 
−
:: Lembrar que dito control poderíamos facelo no propio procedemento almacenado capturando a excepción ou comprobando cun select previo se existe o rexistro relacionado.
 
  
 +
<br />
  
 +
=== Comprobar antes ou capturar a excepción? ===
  
−
* <u>Nota:</u>  
+
Poderiamos comprobar previamente se existen filas relacionadas mediante un <code>SELECT</code> e impedir o borrado desde PHP.
−
:*: Nesta solución non comprobamos se xa existen datos relacionados con ese estado antes de borralos.
 
−
:* Usaremos como base o código feito na solución '''consultar_estados.php''' [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA feito anteriormente na sección de alta].
 
  
 +
Isto pode ser útil para proporcionar unha mensaxe máis específica ao usuario, pero '''non debe substituír as restricións da base de datos'''.
  
 +
Entre a comprobación e o <code>DELETE</code> outra transacción podería modificar os datos.
 +
 +
Unha estratexia habitual consiste en:
 +
 +
:# deseñar correctamente as restricións na base de datos;
 +
:# realizar, cando sexa útil, comprobacións previas na aplicación;
 +
:# intentar a operación;
 +
:# capturar e tratar a posible excepción producida pola base de datos.
 +
 +
<br />
 +
 +
=== Transaccións ===
 +
 +
* Se unha operación lóxica require varias operacións (alta/baixa/modificación) na base de datos e todas deben completarse ou cancelarse xuntas, debemos empregar unha transacción.
 +
: Isto será [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_TRANSACCIONS explicado posteriormente nun punto da wiki].
  
  
 
<br />
 
<br />
−
'''Posible solución Caso Práctico: Baixa'''
+
=== Borrado mediante un procedemento almacenado ===
 +
 
 +
Tamén podemos encapsular a operación nun procedemento almacenado.
 +
 
 +
Por exemplo:
 +
 
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
DELIMITER $$
 +
 
 +
CREATE PROCEDURE BORRAR_ESTADO_PROC(
 +
    IN p_id INT
 +
)
 +
BEGIN
 +
 
 +
    DELETE FROM estados
 +
    WHERE id = p_id;
 +
 
 +
    SELECT ROW_COUNT() AS rexistros_borrados;
 +
 
 +
END$$
 +
 
 +
DELIMITER ;
 +
</syntaxhighlight>
  
−
'''Arquivo bd.php'''. Engadimos o seguinte ao que xa tiñamos.
+
No arquivo <code>bd.php</code> definimos:
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
−
define('BORRAR_ESTADO','DELETE FROM estados where id=:id_estado');
+
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'PROCEDEMENTO_BORRAR_ESTADO',
 +
    'CALL BORRAR_ESTADO_PROC(:id_estado)'
 +
);
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
E desde PHP:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    PROCEDEMENTO_BORRAR_ESTADO
 +
);
  
−
'''Arquivo consultar_estados.php'''
+
$stmt->execute([
−
:::<syntaxhighlight lang="java" enclose="div" highlight="31,40-45,58" >
+
    ':id_estado' => $id
 +
]);
 +
 
 +
$fila = $stmt->fetch();
 +
 
 +
$stmt->closeCursor();
 +
 
 +
$numRexistros = (int) $fila['rexistros_borrados'];
 +
</syntaxhighlight>
 +
 
 +
Neste caso:
 +
 
 +
:* PDO non necesita <code>multi_query()</code>.
 +
:* Recuperamos o conxunto de resultados devolto polo procedemento.
 +
:* Chamamos a <code>closeCursor()</code> para liberar posibles resultados pendentes antes de executar outra orde na mesma conexión.
 +
 
 +
<br />
 +
 
 +
=== mysqli como ampliación ===
 +
 
 +
PDO será o mecanismo principal dos apuntes, pero podemos realizar a mesma operación empregando <code>mysqli</code>.
 +
 
 +
<br />
 +
 
 +
==== Conexión con mysqli ====
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
+
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    mysqli_report(
 +
        MYSQLI_REPORT_ERROR |
 +
        MYSQLI_REPORT_STRICT
 +
    );
 +
 
 +
    $conex = new mysqli(
 +
        SERVIDOR,
 +
        USUARIO,
 +
        PASSWORD,
 +
        BD
 +
    );
 +
 
 +
    $conex->set_charset('utf8mb4');
 +
 
 +
    // Operacións coa base de datos...
  
−
try {
+
} catch (mysqli_sql_exception $e) {
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
 
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
     $erro = "";   // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
     error_log($e->getMessage());
−
?>
 
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
     die('Erro relacionado coa base de datos.');
−
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Xestión de estados</title>
 
−
    </head>
 
−
    <body>
 
−
        <form action="alta_estado.php" method="post">
 
−
            <button type="submit">Novo Estado</button>
 
−
        </form>
 
−
        <table border="1" style="margin-top:20px">
 
−
            <tr>
 
−
                <th>Estado</th>
 
−
                <th>Accións</th>
 
−
            </tr>
 
−
        <?php
 
−
            $result=$conex->query(CONSULTAR_ESTADOS);    // Temos que poñer prepare
 
−
            if ($result && $result->rowCount() > 0) {
 
−
                while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
 
−
                    $nombre = htmlentities($row['descripcion']);
 
−
                    echo "<tr>";
 
−
                    echo "<td>$nombre</td>";
 
−
                    echo "<td>";
 
−
                    printf("<form action='procesar_borrar_estado.php' method='post'>");
 
−
                    printf("<input type='hidden' name='id' value='%s' />",htmlentities($row['id']));
 
−
                    echo '<input onclick="return confirm(\'Estás seguro de que queres borrar este estado?\');" type="submit" value="Borrar" name="btnBorrarEstado" style="background:none;border:none;padding:0;maring:0;color: blue; text-decoration:underline; cursor:pointer; font-size:inherit" />';
 
−
                    echo '</form>';
 
−
                    echo "</td>";
 
−
                    echo "</tr>";
 
−
                }
 
−
            }
 
  
−
            $result = null;
+
} catch (Throwable $e) {
−
        ?>   
 
−
        </table>
 
  
 +
    error_log($e->getMessage());
  
−
        <div>
+
    die('Produciuse un erro na aplicación.');
−
            <?php
 
−
                if(isset($_GET['id'])) printf("<h3>Estado con id %d dado de alta correctamente.</h3>",htmlentities($_GET['id']));
 
−
                if(isset($_GET['msg'])) printf("<h4>%s</h3>",htmlentities($_GET['msg']));
 
−
                if(isset($_GET['error'])) printf("<h2>Houbo un erro: %s</h2>",htmlentities($_GET['error']));
 
−
            ?>
 
−
        </div>   
 
−
       
 
−
    </body>   
 
−
</html>
 
−
<?php
 
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
 
}
 
}
−
?>
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
 +
 +
==== DELETE preparado con mysqli ====
 +
 +
No arquivo <code>bd.php</code>:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'BORRAR_ESTADO_MYSQLI',
 +
    'DELETE FROM estados
 +
    WHERE id = ?'
 +
);
 +
</syntaxhighlight>
 +
 +
E no código PHP:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    BORRAR_ESTADO_MYSQLI
 +
);
 +
 +
$stmt->bind_param(
 +
    'i',
 +
    $id
 +
);
 +
 +
$stmt->execute();
 +
 +
$numRexistros = $stmt->affected_rows;
 +
</syntaxhighlight>
  
 +
A letra <code>i</code> indica que o parámetro é un enteiro.
  
 +
Non necesitamos empregar <code>real_escape_string()</code> nin construír a consulta mediante <code>sprintf()</code>, xa que o valor se envía mediante unha consulta preparada.
  
 
<br />
 
<br />
−
'''Arquivo procesar_borrar_estado.php'''
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="15-17,21,25-29" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
+
==== Tratamento dunha clave foránea con mysqli ====
 +
 
 +
Se MySQL impide o borrado porque a fila segue referenciada, <code>mysqli</code> lanzará unha <code>mysqli_sql_exception</code> cando está activado o modo estrito:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
catch (mysqli_sql_exception $e) {
  
−
try {
+
     error_log($e->getMessage());
−
     $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
 
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
 +
    if ($e->getCode() === 1451) {
  
−
    $stmt = $conex->prepare(BORRAR_ESTADO);    // Temos que poñer prepare
+
        die(
−
    $stmt->bindValue(':id_estado', $_POST['id'], PDO::PARAM_INT);
+
            'Este rexistro está relacionado con outros datos ' .
−
    $result = $stmt->execute();
+
            'e non se pode borrar.'
−
    $msg = "";
+
         );
−
    if ($result) {
 
−
        $numRexistros = $stmt->rowCount();
 
−
         $msg = sprintf("msg=%s%d", urlencode("Num. rexistros borrados:"),$numRexistros);
 
 
     }
 
     }
−
}    // Pecha o try do principio da páxina
+
 
−
catch (PDOException $e) {
+
     die('Erro relacionado coa base de datos.');
−
     $codigoMySQL = $e->errorInfo[1] ?? null;  // Pode non existir nalgúns erros
 
−
    if($e->getCode()==23000){  // Xenérico
 
−
    //if ($codigoMySQL == 1217 || $codigoMySQL == 1451) {      // Especificos de Mysql
 
−
        $error=sprintf("error=%s",urlencode("Este rexistro está relacionado con algún exemplar e non se pode borrar."));
 
−
    }
 
−
    else{
 
−
        $error = sprintf("error=PDO:%s", urlencode($e->getMessage()));
 
−
    }
 
−
} catch (Exception $e) {
 
−
    $error = sprintf("error=ERRO XERAL:%s", urlencode($e->getMessage()));
 
−
}
 
−
finally {
 
−
    $conex = null; // Pechar a conexión
 
 
}
 
}
−
$url = sprintf("consultar_estados.php?%s&%s", $msg, $error);
 
−
header("Location:" . $url);
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
 +
 +
=== Exercicios propostos ===
 +
 +
* De todos os exercicios fai dúas versións, unha empregando a orde SQL INSERT e outra chamando a un procedemento almacenado de Mysql.
 +
 +
 +
<br />
 +
==== Baixa de Autores ====
 +
 +
* Ao carón de cada autor, amosa o número de libros relacionados con dito autor. En caso de que non teña libros amosarás un botón para dalo de baixa.
 +
: Isto non quita que debas comprobar o posible erro de que o autor esté relacionado con otros libros.
 +
: Debe pedir confirmación antes de borrar o autor. Amosa na mensaxe de javascript o nome do autor a dar de baixa.
 +
 +
: Fai coma no examplo e crea unha páxina php procesar_borrar_xxx.php para xestionar a baixa.
 +
 +
 +
 +
 +
 +
<br />
 +
 +
==== Baixa de Libros ====
  
 +
* Ao carón de cada libro, amosa un botón para dalo de baixa.
 +
: Se o libro ten exemplares relacionados, amosarás unha mensaxe de erro indicando cantos exemplares ten o libro.
 +
: Se o libro ten autores relacionados, amosarás unha mensaxe indicando o erro.
 +
: Debe pedir confirmación antes de borrar o libro. Amosa na mensaxe de javascript o nome do libro a dar de baixa.
  
 +
: Fai coma no examplo e crea unha páxina php procesar_borrar_xxx.php para xestionar a baixa.
 +
 +
 +
 +
 +
<br />
 +
==== Baixa de Exemplares ====
 +
 +
* Ao carón de cada exemplar, amosa un botón para dalo de baixa.
 +
: Debe pedir confirmación antes de borrar o exemplar. Amosa na mensaxe de javascript o nome do libro a dar de baixa.
 +
 +
: Fai coma no examplo e crea unha páxina php procesar_borrar_xxx.php para xestionar a baixa.
 +
 +
 +
 +
 +
 +
 +
 +
 +
<br />
  
  
 +
=== Resumo ===
  
 +
* En PHP 8.5 empregaremos '''PDO como mecanismo principal''' para realizar os borrados.
 +
* As consultas gardaranse en <code>/var/www/confBD/bd.php</code>.
 +
* Os criterios variables dun <code>DELETE</code> enviaranse mediante consultas preparadas.
 +
* Non construiremos a orde SQL concatenando ou interpolando datos recibidos do usuario.
 +
* Empregaremos <code>POST</code> para solicitar operacións de borrado.
 +
* Un formulario <code>POST</code> non substitúe a protección CSRF.
 +
* Os identificadores recibidos deben validarse no servidor.
 +
* Un campo <code>hidden</code> pode ser manipulado polo usuario.
 +
* A validación dun identificador non substitúe as comprobacións de autorización.
 +
* <code>PDOStatement::rowCount()</code> permite coñecer cantas filas afectou un <code>DELETE</code>.
 +
* Un resultado de <code>0</code> filas afectadas non implica necesariamente un erro.
 +
* As claves foráneas deben garantir a integridade referencial na base de datos.
 +
* En PDO, <code>PDOException::getCode()</code> devolve o SQLSTATE.
 +
* O SQLSTATE <code>23000</code> representa unha violación de integridade.
 +
* En MySQL, o código nativo <code>1451</code> identifica especificamente o intento de borrar ou modificar unha fila pai que segue referenciada.
 +
* Os detalles técnicos das excepcións deben gardarse no log e non mostrarse directamente aos usuarios.
 +
* <code>mysqli</code> explicarase como alternativa adicional, tamén empregando consultas preparadas.
  
−
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2025).
+
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2026).

Revisión actual del 08:52 6 oct 2026

Introdución

Nesta unidade veremos como realizar operacións de baixa sobre unha base de datos MySQL/MariaDB desde PHP 8.5.

O mecanismo principal empregado nos apuntes será PDO (PHP Data Objects). Ao final inclúese tamén unha explicación equivalente empregando mysqli como ampliación.

  • Requirimentos previos:
  • Ter creada a base de datos indicada nunha sección anterior.
  • Ter gardados os parámetros de conexión nun arquivo separado, fóra da ruta /var/www/html.
  • Ter instalada e habilitada a extensión pdo_mysql.
  • Os exemplos están pensados para PHP 8.5.
  • Partiremos do código realizado anteriormente na sección de altas.
  • NOTA IMPORTANTE: nesta unidade realizaremos os borrados mediante peticións POST. Non empregaremos unha ligazón GET para executar directamente unha operación que modifica datos.
A protección fronte a ataques CSRF (Cross-Site Request Forgery) incorporarase cando se estuden as sesións. Unha petición POST por si soa non evita un ataque CSRF.


  • CONSIDERACIÓNS:
  • Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos)
  • Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como vimos nun punto anterior desta sección.
  • Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas.
  • En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes.
  • Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...).
  • Lembrar que tamén podedes activar o debugger como está indicado nesta sección da wiki.





Organización dos exemplos

No arquivo /var/www/confBD/bd.php, situado fóra da ruta pública /var/www/html, gardaremos os datos da conexión e as consultas:

<?php

define('SERVIDOR', 'localhost');
define('USUARIO', 'user_php');
define('PASSWORD', 'user_php');
define('BD', 'php');

define(
    'NOME_DA_CONSULTA',
    'SELECT col1, col2
     FROM taboa
     ORDER BY col1'
);

Nas páxinas PHP que accedan á base de datos empregaremos como base o seguinte esquema:

<?php

try {

    require '/var/www/confBD/bd.php';

    $dsn = sprintf(
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
        SERVIDOR,
        BD
    );

    $conex = new PDO(
        $dsn,
        USUARIO,
        PASSWORD,
        [
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]
    );

    // Operacións coa base de datos...

} catch (PDOException $e) {

    error_log($e->getMessage());

    die('Erro relacionado coa base de datos.');

} catch (Throwable $e) {

    error_log($e->getMessage());

    die('Produciuse un erro na aplicación.');
}


Baixa de rexistros: DELETE

  • O proceso de baixa realízase mediante a orde SQL DELETE.

Por exemplo:

DELETE FROM estados
WHERE id = 10;
  • Podemos eliminar unha fila ou un conxunto de filas empregando a condición indicada na cláusula WHERE.
  • IMPORTANTE: debemos ter especial coidado coa cláusula WHERE. A seguinte orde eliminaría todas as filas da táboa:
DELETE FROM estados;
  • Desde PHP, sempre que o criterio de borrado conteña datos variables, empregaremos unha consulta preparada:
$stmt = $conex->prepare(
    'DELETE FROM estados
     WHERE id = :id_estado'
);

$stmt->execute([
    ':id_estado' => $id
]);

Non construiremos a orde SQL introducindo directamente o identificador mediante concatenación ou sprintf().


Borrado e integridade referencial

Nas bases de datos relacionais un rexistro pode estar relacionado con rexistros doutras táboas mediante claves foráneas.

Ao definir unha clave foránea podemos indicar que debe ocorrer cando se elimina ou modifica a fila referenciada.

As opcións máis habituais son:

  • RESTRICT: impide o borrado se existen filas relacionadas.
  • NO ACTION: en MySQL ten un comportamento equivalente a RESTRICT para este propósito.
  • CASCADE: elimina automaticamente as filas dependentes.
  • SET NULL: establece a clave foránea a NULL, sempre que a columna o permita.

Por exemplo:

CREATE TABLE LIBROS_AUTORES (
    libro_id INT NOT NULL,
    autor_id INT NOT NULL,

    PRIMARY KEY (libro_id, autor_id),

    CONSTRAINT fk_libros_autores_autores
        FOREIGN KEY (autor_id)
        REFERENCES AUTORES (id_autor)
        ON DELETE RESTRICT
        ON UPDATE CASCADE,

    CONSTRAINT fk_libros_autores_libros
        FOREIGN KEY (libro_id)
        REFERENCES LIBROS (id_libro)
        ON DELETE RESTRICT
        ON UPDATE CASCADE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4;

Neste exemplo:

  • Non se poderá eliminar un autor se existen filas de LIBROS_AUTORES que fan referencia a ese autor.
  • Se se modifica a clave referenciada e a relación está definida con ON UPDATE CASCADE, a modificación propágase ás filas relacionadas.

Polo tanto, ao deseñar unha aplicación debemos decidir se:

  • impedimos o borrado e informamos ao usuario;
  • eliminamos previamente os datos dependentes;
  • empregamos ON DELETE CASCADE;
  • ou aplicamos outra regra de integridade coherente co modelo de datos.

A decisión forma parte tamén do deseño da base de datos, non só do código PHP.


Caso práctico: baixa dun estado

Imos continuar o mantemento da táboa ESTADOS realizado na sección de altas.

O obxectivo será:

  • Amosar os estados existentes.
  • Engadir unha opción Borrar en cada fila.
  • Enviar o identificador mediante un formulario POST.
  • Validar no servidor que o identificador recibido é correcto.
  • Executar un DELETE mediante unha consulta preparada.
  • Informar do número de rexistros eliminados.
  • Tratar o caso no que a fila non pode eliminarse debido a unha clave foránea.


Arquivo /var/www/confBD/bd.php

Engadimos a seguinte consulta ao arquivo empregado nas seccións anteriores:

define(
    'BORRAR_ESTADO',
    'DELETE FROM estados
     WHERE id = :id_estado'
);

Fixarse en que o identificador non se introduce directamente na cadea SQL. Empregamos o parámetro :id_estado.


Modificación de consultar_estados.php

Partimos da páxina creada na sección de alta e engadimos unha columna de accións.

<?php

try {

    require '/var/www/confBD/bd.php';

    $dsn = sprintf(
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
        SERVIDOR,
        BD
    );

    $conex = new PDO(
        $dsn,
        USUARIO,
        PASSWORD,
        [
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]
    );

    $stmt = $conex->query(CONSULTAR_ESTADOS);
    $estados = $stmt->fetchAll();

} catch (PDOException $e) {

    error_log($e->getMessage());

    die('Erro relacionado coa base de datos.');

} catch (Throwable $e) {

    error_log($e->getMessage());

    die('Produciuse un erro na aplicación.');
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Xestión de estados</title>
</head>
<body>

    <h1>Xestión de estados</h1>

    <form action="alta_estado.php" method="get">
        <button type="submit">Novo estado</button>
    </form>

    <table>
        <thead>
            <tr>
                <th>Id</th>
                <th>Descrición</th>
                <th>Accións</th>
            </tr>
        </thead>

        <tbody>

        <?php foreach ($estados as $estado): ?>

            <tr>
                <td>
                    <?= htmlspecialchars(
                        (string) $estado['id'],
                        ENT_QUOTES | ENT_SUBSTITUTE,
                        'UTF-8'
                    ) ?>
                </td>

                <td>
                    <?= htmlspecialchars(
                        $estado['descripcion'],
                        ENT_QUOTES | ENT_SUBSTITUTE,
                        'UTF-8'
                    ) ?>
                </td>

                <td>
                    <form
                        action="procesar_borrar_estado.php"
                        method="post"
                        onsubmit="return confirm(
                            'Estás seguro de que queres borrar este estado?'
                        );"
                    >
                        <input
                            type="hidden"
                            name="id"
                            value="<?= htmlspecialchars(
                                (string) $estado['id'],
                                ENT_QUOTES | ENT_SUBSTITUTE,
                                'UTF-8'
                            ) ?>"
                        >

                        <button
                            type="submit"
                            name="btnBorrarEstado"
                            value="BORRAR"
                        >
                            Borrar
                        </button>
                    </form>
                </td>
            </tr>

        <?php endforeach; ?>

        </tbody>
    </table>

    <?php if (isset($_GET['id'])): ?>
        <p>
            Estado con id
            <?= htmlspecialchars(
                $_GET['id'],
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            ) ?>
            dado de alta correctamente.
        </p>
    <?php endif; ?>

    <?php if (isset($_GET['msg'])): ?>
        <p>
            <?= htmlspecialchars(
                $_GET['msg'],
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            ) ?>
        </p>
    <?php endif; ?>

    <?php if (isset($_GET['error'])): ?>
        <p>
            <?= htmlspecialchars(
                $_GET['error'],
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            ) ?>
        </p>
    <?php endif; ?>

</body>
</html>

Neste exemplo empregamos un formulario POST para a operación de borrado.

Non empregaremos unha URL do tipo:

<a href="procesar_borrar_estado.php?id=5">
    Borrar
</a>

porque unha petición GET non debería empregarse para provocar directamente unha modificación dos datos.

A chamada JavaScript a confirm() evita borrados accidentais por parte do usuario, pero non constitúe unha medida de seguridade. A validación e autorización deben realizarse sempre no servidor.


Arquivo procesar_borrar_estado.php

<?php

try {

    require '/var/www/confBD/bd.php';

    $dsn = sprintf(
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
        SERVIDOR,
        BD
    );

    $conex = new PDO(
        $dsn,
        USUARIO,
        PASSWORD,
        [
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]
    );

    if (
        !isset($_POST['btnBorrarEstado']) ||
        $_POST['btnBorrarEstado'] !== 'BORRAR'
    ) {
        throw new RuntimeException(
            'Operación non válida.'
        );
    }

    /*
     * Un campo hidden tamén pode ser modificado polo usuario,
     * polo que debemos validar o identificador no servidor.
     */
    $id = filter_input(
        INPUT_POST,
        'id',
        FILTER_VALIDATE_INT,
        [
            'options' => [
                'min_range' => 1
            ]
        ]
    );

    if ($id === false || $id === null) {

        header(
            'Location: consultar_estados.php?error=' .
            urlencode('O identificador non é válido.')
        );
        exit;
    }

    $stmt = $conex->prepare(BORRAR_ESTADO);

    $stmt->execute([
        ':id_estado' => $id
    ]);

    $numRexistros = $stmt->rowCount();

    if ($numRexistros === 1) {

        $mensaxe = 'Rexistro borrado correctamente.';

    } else {

        /*
         * Non se produciu un erro SQL:
         * simplemente non existía unha fila co id indicado.
         */
        $mensaxe =
            'Non se atopou o rexistro que se quería borrar.';
    }

    header(
        'Location: consultar_estados.php?msg=' .
        urlencode($mensaxe)
    );
    exit;

} catch (PDOException $e) {

    /*
     * getCode() devolve o SQLSTATE.
     * errorInfo[1] contén o código específico do driver.
     */
    $sqlState = $e->getCode();
    $codigoMySQL = $e->errorInfo[1] ?? null;

    error_log($e->getMessage());

    if (
        $sqlState === '23000' &&
        $codigoMySQL === 1451   // Podemos saber qué código é se imos a mysql e intentamos executar a mesma orde SQL Delete
    ) {

        $mensaxe =
            'Este rexistro está relacionado con outros datos ' .
            'e non se pode borrar.';

    } else {

        $mensaxe =
            'Erro relacionado coa base de datos.';
    }

    header(
        'Location: consultar_estados.php?error=' .
        urlencode($mensaxe)
    );
    exit;

} catch (Throwable $e) {

    error_log($e->getMessage());

    header(
        'Location: consultar_estados.php?error=' .
        urlencode('Produciuse un erro na aplicación.')
    );
    exit;
}


Validación do identificador

O valor dun campo oculto dun formulario non é un dato de confianza.

Un usuario pode modificar manualmente a petición HTTP, polo que non debemos dar por feito que $_POST['id'] contén necesariamente un número enteiro válido.

Por iso empregamos:

$id = filter_input(
    INPUT_POST,
    'id',
    FILTER_VALIDATE_INT,
    [
        'options' => [
            'min_range' => 1
        ]
    ]
);

Desta maneira comprobamos que o identificador recibido é un enteiro maior ou igual que 1.

IMPORTANTE: validar que o identificador sexa correcto non implica que o usuario teña permiso para borrar ese rexistro. Nunha aplicación real tamén será necesario comprobar a autorización.


Número de rexistros borrados

Despois de executar:

$stmt->execute([
    ':id_estado' => $id
]);

podemos obter o número de filas afectadas mediante:

$numRexistros = $stmt->rowCount();

Para unha operación DELETE sobre un identificador único:

  • 1: existía a fila e foi eliminada.
  • 0: non existía ningunha fila que cumprise a condición.

Que rowCount() devolva 0 non implica necesariamente un erro.


Tratamento dos erros de integridade referencial

Se intentamos borrar unha fila que segue referenciada por unha clave foránea definida con RESTRICT ou NO ACTION, MySQL impedirá o borrado.

PDO lanzará unha PDOException porque configuramos:

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

Nunha PDOException podemos consultar:

$e->getCode()

que devolve o código SQLSTATE.

Para unha violación dunha restrición de integridade o SQLSTATE é:

23000

Porén, 23000 é unha categoría xenérica de erros de integridade.

Se queremos saber se MySQL está indicando especificamente que unha fila pai segue referenciada, podemos consultar tamén:

$codigoMySQL = $e->errorInfo[1] ?? null;

e comprobar:

if (
    $e->getCode() === '23000' &&
    $codigoMySQL === 1451
) {
    // A fila segue referenciada por unha clave foránea.
}

O código MySQL 1451 corresponde ao erro:

Cannot delete or update a parent row:
a foreign key constraint fails

IMPORTANTE: non debemos enviar directamente ao usuario:

$e->getMessage()

porque pode revelar nomes de táboas, columnas ou outros detalles internos.

Rexistraremos a información técnica:

error_log($e->getMessage());

e amosaremos ao usuario unha mensaxe comprensible.


Comprobar antes ou capturar a excepción?

Poderiamos comprobar previamente se existen filas relacionadas mediante un SELECT e impedir o borrado desde PHP.

Isto pode ser útil para proporcionar unha mensaxe máis específica ao usuario, pero non debe substituír as restricións da base de datos.

Entre a comprobación e o DELETE outra transacción podería modificar os datos.

Unha estratexia habitual consiste en:

  1. deseñar correctamente as restricións na base de datos;
  2. realizar, cando sexa útil, comprobacións previas na aplicación;
  3. intentar a operación;
  4. capturar e tratar a posible excepción producida pola base de datos.


Transaccións

  • Se unha operación lóxica require varias operacións (alta/baixa/modificación) na base de datos e todas deben completarse ou cancelarse xuntas, debemos empregar unha transacción.
Isto será explicado posteriormente nun punto da wiki.



Borrado mediante un procedemento almacenado

Tamén podemos encapsular a operación nun procedemento almacenado.

Por exemplo:

DELIMITER $$

CREATE PROCEDURE BORRAR_ESTADO_PROC(
    IN p_id INT
)
BEGIN

    DELETE FROM estados
    WHERE id = p_id;

    SELECT ROW_COUNT() AS rexistros_borrados;

END$$

DELIMITER ;

No arquivo bd.php definimos:

define(
    'PROCEDEMENTO_BORRAR_ESTADO',
    'CALL BORRAR_ESTADO_PROC(:id_estado)'
);

E desde PHP:

$stmt = $conex->prepare(
    PROCEDEMENTO_BORRAR_ESTADO
);

$stmt->execute([
    ':id_estado' => $id
]);

$fila = $stmt->fetch();

$stmt->closeCursor();

$numRexistros = (int) $fila['rexistros_borrados'];

Neste caso:

  • PDO non necesita multi_query().
  • Recuperamos o conxunto de resultados devolto polo procedemento.
  • Chamamos a closeCursor() para liberar posibles resultados pendentes antes de executar outra orde na mesma conexión.


mysqli como ampliación

PDO será o mecanismo principal dos apuntes, pero podemos realizar a mesma operación empregando mysqli.


Conexión con mysqli

<?php

try {

    require '/var/www/confBD/bd.php';

    mysqli_report(
        MYSQLI_REPORT_ERROR |
        MYSQLI_REPORT_STRICT
    );

    $conex = new mysqli(
        SERVIDOR,
        USUARIO,
        PASSWORD,
        BD
    );

    $conex->set_charset('utf8mb4');

    // Operacións coa base de datos...

} catch (mysqli_sql_exception $e) {

    error_log($e->getMessage());

    die('Erro relacionado coa base de datos.');

} catch (Throwable $e) {

    error_log($e->getMessage());

    die('Produciuse un erro na aplicación.');
}


DELETE preparado con mysqli

No arquivo bd.php:

define(
    'BORRAR_ESTADO_MYSQLI',
    'DELETE FROM estados
     WHERE id = ?'
);

E no código PHP:

$stmt = $conex->prepare(
    BORRAR_ESTADO_MYSQLI
);

$stmt->bind_param(
    'i',
    $id
);

$stmt->execute();

$numRexistros = $stmt->affected_rows;

A letra i indica que o parámetro é un enteiro.

Non necesitamos empregar real_escape_string() nin construír a consulta mediante sprintf(), xa que o valor se envía mediante unha consulta preparada.


Tratamento dunha clave foránea con mysqli

Se MySQL impide o borrado porque a fila segue referenciada, mysqli lanzará unha mysqli_sql_exception cando está activado o modo estrito:

catch (mysqli_sql_exception $e) {

    error_log($e->getMessage());

    if ($e->getCode() === 1451) {

        die(
            'Este rexistro está relacionado con outros datos ' .
            'e non se pode borrar.'
        );
    }

    die('Erro relacionado coa base de datos.');
}


Exercicios propostos

  • De todos os exercicios fai dúas versións, unha empregando a orde SQL INSERT e outra chamando a un procedemento almacenado de Mysql.



Baixa de Autores

  • Ao carón de cada autor, amosa o número de libros relacionados con dito autor. En caso de que non teña libros amosarás un botón para dalo de baixa.
Isto non quita que debas comprobar o posible erro de que o autor esté relacionado con otros libros.
Debe pedir confirmación antes de borrar o autor. Amosa na mensaxe de javascript o nome do autor a dar de baixa.
Fai coma no examplo e crea unha páxina php procesar_borrar_xxx.php para xestionar a baixa.




Baixa de Libros

  • Ao carón de cada libro, amosa un botón para dalo de baixa.
Se o libro ten exemplares relacionados, amosarás unha mensaxe de erro indicando cantos exemplares ten o libro.
Se o libro ten autores relacionados, amosarás unha mensaxe indicando o erro.
Debe pedir confirmación antes de borrar o libro. Amosa na mensaxe de javascript o nome do libro a dar de baixa.
Fai coma no examplo e crea unha páxina php procesar_borrar_xxx.php para xestionar a baixa.




Baixa de Exemplares

  • Ao carón de cada exemplar, amosa un botón para dalo de baixa.
Debe pedir confirmación antes de borrar o exemplar. Amosa na mensaxe de javascript o nome do libro a dar de baixa.
Fai coma no examplo e crea unha páxina php procesar_borrar_xxx.php para xestionar a baixa.







Resumo

  • En PHP 8.5 empregaremos PDO como mecanismo principal para realizar os borrados.
  • As consultas gardaranse en /var/www/confBD/bd.php.
  • Os criterios variables dun DELETE enviaranse mediante consultas preparadas.
  • Non construiremos a orde SQL concatenando ou interpolando datos recibidos do usuario.
  • Empregaremos POST para solicitar operacións de borrado.
  • Un formulario POST non substitúe a protección CSRF.
  • Os identificadores recibidos deben validarse no servidor.
  • Un campo hidden pode ser manipulado polo usuario.
  • A validación dun identificador non substitúe as comprobacións de autorización.
  • PDOStatement::rowCount() permite coñecer cantas filas afectou un DELETE.
  • Un resultado de 0 filas afectadas non implica necesariamente un erro.
  • As claves foráneas deben garantir a integridade referencial na base de datos.
  • En PDO, PDOException::getCode() devolve o SQLSTATE.
  • O SQLSTATE 23000 representa unha violación de integridade.
  • En MySQL, o código nativo 1451 identifica especificamente o intento de borrar ou modificar unha fila pai que segue referenciada.
  • Os detalles técnicos das excepcións deben gardarse no log e non mostrarse directamente aos usuarios.
  • mysqli explicarase como alternativa adicional, tamén empregando consultas preparadas.


-- Ángel D. Fernández González -- (2026).