Diferencia entre revisiones de «PHP UD3 OP BAIXA»
(Página creada con « * 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…») |
(→Resumo) |
||
| (No se muestran 10 ediciones intermedias del mismo usuario) | |||
| Línea 1: | Línea 1: | ||
| + | === 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. | |
| − | * | + | * '''<u>Requirimentos previos:</u>''' |
| + | :* 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]. | |
| − | |||
| − | |||
| − | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==== 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()); | ||
| + | |||
| + | die('Produciuse un erro na aplicación.'); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Baixa de rexistros: DELETE === | ||
| + | |||
| + | * O proceso de baixa realízase mediante a orde SQL <code>DELETE</code>. | ||
| + | |||
| + | Por exemplo: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
| + | DELETE FROM estados | ||
| + | WHERE id = 10; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | * Podemos eliminar unha fila ou un conxunto de filas empregando a condición indicada na cláusula <code>WHERE</code>. | ||
| + | |||
| + | * '''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> | ||
| + | |||
| + | * 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' | ||
| + | ); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':id_estado' => $id | ||
| + | ]); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Non construiremos a orde SQL introducindo directamente o identificador mediante concatenación ou <code>sprintf()</code>. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === 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: | ||
| + | |||
| + | :* <code>RESTRICT</code>: impide o borrado se existen filas relacionadas. | ||
| + | :* <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. | ||
| + | :* <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> | ||
| + | |||
| + | 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: | ||
| + | |||
| + | :* impedimos o borrado e informamos ao usuario; | ||
| + | :* eliminamos previamente os datos dependentes; | ||
| + | :* empregamos <code>ON DELETE CASCADE</code>; | ||
| + | :* 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. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Caso práctico: baixa dun estado === | ||
| + | |||
| + | 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 ==== | ||
| + | |||
| + | Engadimos a seguinte consulta ao arquivo empregado nas seccións anteriores: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | define( | ||
| + | 'BORRAR_ESTADO', | ||
| + | 'DELETE FROM estados | ||
| + | WHERE id = :id_estado' | ||
| + | ); | ||
| + | </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. | ||
| + | |||
| + | ::<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 | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | $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> | ||
| + | </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 | ||
| + | |||
| + | 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; | ||
| + | } | ||
| + | </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> | </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 /> | |
| + | === Tratamento dos erros de integridade referencial === | ||
| − | + | 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. | |
| − | : | + | 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> | </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. | ||
| − | + | Se queremos saber se MySQL está indicando especificamente que unha fila pai segue referenciada, podemos consultar tamén: | |
| − | : | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $codigoMySQL = $e->errorInfo[1] ?? null; | ||
| + | </syntaxhighlight> | ||
| + | e comprobar: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| + | if ( | ||
| + | $e->getCode() === '23000' && | ||
| + | $codigoMySQL === 1451 | ||
| + | ) { | ||
| + | // A fila segue referenciada por unha clave foránea. | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | 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> | ||
| − | + | porque pode revelar nomes de táboas, columnas ou outros detalles internos. | |
| − | |||
| − | |||
| − | + | Rexistraremos a información técnica: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| + | error_log($e->getMessage()); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | e amosaremos ao usuario unha mensaxe comprensible. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Comprobar antes ou capturar a excepción? === | ||
| + | |||
| + | Poderiamos comprobar previamente se existen filas relacionadas mediante un <code>SELECT</code> 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 <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 /> | ||
| + | === Borrado mediante un procedemento almacenado === | ||
| + | |||
| + | Tamén podemos encapsular a operación nun procedemento almacenado. | ||
| + | |||
| + | Por exemplo: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
DELIMITER $$ | DELIMITER $$ | ||
| − | CREATE | + | CREATE PROCEDURE BORRAR_ESTADO_PROC( |
| + | IN p_id INT | ||
| + | ) | ||
BEGIN | BEGIN | ||
| − | + | DELETE FROM estados | |
| − | + | WHERE id = p_id; | |
| − | END | + | SELECT ROW_COUNT() AS rexistros_borrados; |
| + | |||
| + | END$$ | ||
| + | |||
| + | DELIMITER ; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | No arquivo <code>bd.php</code> definimos: | ||
| + | |||
| + | ::<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 | ||
| + | ); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':id_estado' => $id | ||
| + | ]); | ||
| + | |||
| + | $fila = $stmt->fetch(); | ||
| + | $stmt->closeCursor(); | ||
| − | + | $numRexistros = (int) $fila['rexistros_borrados']; | |
| − | |||
| − | |||
| − | |||
</syntaxhighlight> | </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 | ||
| − | require | + | 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'); | |
| − | $conex->set_charset(' | ||
| − | + | // 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.'); | ||
} | } | ||
| − | + | </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 /> | ||
| + | |||
| + | ==== 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) { | ||
| − | + | 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.'); | ||
| + | } | ||
</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. | ||
| Línea 269: | Línea 876: | ||
| + | <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]] -- ( | + | <br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2026). |
Revisión actual del 08:52 6 oct 2026
Sumario
- 1 Introdución
- 2 Baixa de rexistros: DELETE
- 3 Borrado e integridade referencial
- 4 Caso práctico: baixa dun estado
- 5 Tratamento dos erros de integridade referencial
- 6 Comprobar antes ou capturar a excepción?
- 7 Transaccións
- 8 Borrado mediante un procedemento almacenado
- 9 mysqli como ampliación
- 10 Exercicios propostos
- 11 Resumo
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ónGETpara 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
POSTpor 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 aRESTRICTpara este propósito.CASCADE: elimina automaticamente as filas dependentes.SET NULL: establece a clave foránea aNULL, 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_AUTORESque 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.
- Non se poderá eliminar un autor se existen filas de
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
DELETEmediante 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:
- 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.
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.
- PDO non necesita
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
DELETEenviaranse mediante consultas preparadas. - Non construiremos a orde SQL concatenando ou interpolando datos recibidos do usuario.
- Empregaremos
POSTpara solicitar operacións de borrado. - Un formulario
POSTnon substitúe a protección CSRF. - Os identificadores recibidos deben validarse no servidor.
- Un campo
hiddenpode 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 unDELETE.- Un resultado de
0filas 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
23000representa unha violación de integridade. - En MySQL, o código nativo
1451identifica 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.
mysqliexplicarase como alternativa adicional, tamén empregando consultas preparadas.
-- Ángel D. Fernández González -- (2026).