Diferencia entre revisiones de «PHP UD3 OP BAIXA»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 8 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]
 
−
:* 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].
 
  
 +
* '''<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:''' [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.
+
* '''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 />
 
<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)
+
==== 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>
  
−
* 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.
+
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'''.
  
−
* '''IMPORTANTE:''' Temos que ter en conta que nas bases de datos relacionais, dar de baixa un rexistro pode implicar borrar rexistros relacionados.
+
<br />
−
: 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].
 
  
 +
==== Número de rexistros borrados ====
  
−
: Por exemplo, na táboa LIBROS_AUTORES:
+
Despois de executar:
−
:::<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="php" enclose="div">
 +
$stmt->execute([
 +
    ':id_estado' => $id
 +
]);
 
</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 obter o número de filas afectadas mediante:
−
: Se actualizamos a clave primaria da táboa AUTORES, actualizará a clave na táboa AUTORES_LIBROS (parte ON UPDATE CASCADE).
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$numRexistros = $stmt->rowCount();
 +
</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.
+
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.
  
−
=== Caso Práctico: Baixa ===
+
Que <code>rowCount()</code> devolva <code>0</code> non implica necesariamente un erro.
  
−
* Imos implementar esta opción na páxina '''Xestionar_Estados.php'''.
+
<br />
  
 +
=== Tratamento dos erros de integridade referencial ===
  
−
: Cargaremos nunha <select> todos os estados e premendo o botón 'Baixa' daremos de baixa o estado seleccionado.
+
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.
  
−
: Faremos uso dun procedemento almacenado de nome '''BORRAR_ESTADO''' que terá como parámetro o id do estado a borrar e fará a sentenza sql DELETE.
+
PDO lanzará unha <code>PDOException</code> porque configuramos:
−
: Faremos uso do procedemento almacenado 'CONSULTAR_ESTADOS' que devolverá nun select o id e descrición dos estados para cargar na lista.
 
  
−
: Nesta solución non comprobamos se xa existen datos relacionados con ese estado antes de borralos e usaremos como base o código feito na solución '''Xestionar_Estados_Proc.php''' feito anteriormente.
+
::<syntaxhighlight lang="php" enclose="div">
 +
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
</syntaxhighlight>
  
 +
Nunha <code>PDOException</code> podemos consultar:
  
−
* 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="php" enclose="div">
−
+
$e->getCode()
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="9" >
 
−
                    if ($conex->multi_query($proc)){
 
−
                        $result = $conex->store_result();
 
−
                        if ($result && ($result->num_rows>0)){
 
−
                            while($fila=$result->fetch_assoc()){
 
−
                                // Procesamos cada fila
 
−
                            }
 
−
                            $result->free();
 
−
                        }
 
−
                        while($conex->next_result() && $conex->store_result());
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
que devolve o código '''SQLSTATE'''.
  
 +
Para unha violación dunha restrición de integridade o SQLSTATE é:
  
−
* 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'.
+
::<syntaxhighlight lang="text" enclose="div">
−
: 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.
+
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:
  
−
* <u>Nota:</u> Lembrar que nesta solución estamos a utilizar a chamada a '''multi_query''' pero se podería utilizar a chamada a '''query''' no caso de que o procedemento non devolva resultados (teña un select que queiramos recoller en PHP).
+
::<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>
  
−
'''Posible solución Caso Práctico: Baixa'''
+
'''IMPORTANTE:''' non debemos enviar directamente ao usuario:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$e->getMessage()
 +
</syntaxhighlight>
  
−
'''Procedemento almacenado en Mysql: CONSULTAR_ESTADOS'''
+
porque pode revelar nomes de táboas, columnas ou outros detalles internos.
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_ESTADOS`()
+
Rexistraremos a información técnica:
−
BEGIN
 
−
SELECT id_estado,descripcion
 
−
FROM ESTADOS
 
−
ORDER BY descripcion;
 
  
−
END
+
::<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:
  
−
'''Procedemento almacenado en Mysql: BORRAR_ESTADO'''
+
::<syntaxhighlight lang="sql" enclose="div">
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `BORRAR_ESTADO`(_id_estado int)
+
CREATE PROCEDURE BORRAR_ESTADO_PROC(
 +
    IN p_id INT
 +
)
 
BEGIN
 
BEGIN
  
−
DELETE FROM ESTADOS
+
    DELETE FROM estados
−
WHERE id_estado = _id_estado;
+
    WHERE id = p_id;
 +
 
 +
    SELECT ROW_COUNT() AS rexistros_borrados;
 +
 
 +
END$$
 +
 
 +
DELIMITER ;
 +
</syntaxhighlight>
 +
 
 +
No arquivo <code>bd.php</code> definimos:
  
−
END
+
::<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
 +
]);
  
−
'''Arquivo bd.inc.php'''
+
$fila = $stmt->fetch();
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
 
−
define ('PROCEDEMENTO_BORRAR_ESTADO',"BORRAR_ESTADO(%d)");
+
$stmt->closeCursor();
−
define ('PROCEDEMENTO_CONSULTAR_ESTADOS',"CONSULTAR_ESTADOS()");
+
 
 +
$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 />
  
−
'''Arquivo Xestionar_Estados_Proc.php'''
+
=== mysqli como ampliación ===
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="36-57,69-86" >
+
 
 +
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 ('/var/www/confBD/bd.inc.php');
+
try {
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
    require '/var/www/confBD/bd.php';
−
$conex->set_charset('utf8');
 
  
−
if ($conex->connect_error){
+
    mysqli_report(
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+
        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.');
 
}
 
}
−
$erro="";   // Variable que a usaremos para amosar os posibles erros que poidan darse
+
</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) {
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
     error_log($e->getMessage());
−
     "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>Xestionar t&aacute;boa Estados BD</title>
 
−
    </head>
 
  
−
     <body>
+
     if ($e->getCode() === 1451) {
−
        <!-- Esta parte poderíase facer con javascript -->
 
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
 
−
            <h2>Escolle a operación a facer sobre a táboa ESTADOS</h2>
 
−
            <input <?php echo (isset($_POST['rbtnOperacion']) && $_POST['rbtnOperacion']=='ALTA') ? "checked='checked'" : "";  ?>
 
−
                onclick='javascript:this.form-submit()' type='radio' name="rbtnOperacion" value='ALTA'>ALTA</input>
 
−
            <input <?php echo (isset($_POST['rbtnOperacion']) && $_POST['rbtnOperacion']=='BAIXA') ? "checked='checked'" : "";  ?>
 
−
                onclick='javascript:this.form-submit()'  type='radio' name="rbtnOperacion" value='BAIXA'>BAIXA</input>
 
−
            <input <?php echo (isset($_POST['rbtnOperacion']) && $_POST['rbtnOperacion']=='MODIFICACION') ? "checked='checked'" : "";  ?>
 
−
                onclick='javascript:this.form-submit()'  type='radio' name="rbtnOperacion" value='MODIFICACION'>MODIFICACI&Oacute;N</input>
 
−
        <?php 
 
  
−
         // Operación BAIXA => TEN QUE IR ANTES XA QUE DESPOIS CARGAMOS A LISTA COS ESTADOS!!!
+
         die(
−
        if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion']=='BAIXA'){ 
+
             'Este rexistro está relacionado con outros datos ' .
−
             $id_estado = $_POST['lstEstados'];
+
             'e non se pode borrar.'
−
            if (!filter_var($id_estado,FILTER_VALIDATE_INT)){
+
         );
−
                $error='<h4>O estado indicado non &eacute; v&aacute;lido...</h4>';
+
    }
−
            }
 
−
             else {
 
−
                $proc = sprintf(PROCEDEMENTO_BORRAR_ESTADO,$conex->mysql_real_escape($id_estado));
 
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    printf("<h4>Num. rexistros borrados:%d</h4>", $conex->affected_rows);
 
−
                    echo "<script>alert('Rexistro dado de baixa correctamente');</script>";
 
−
                    while($conex->next_result() && $conex->store_result());
 
−
                }
 
−
                else{
 
−
                    echo "<h5>Falló CALL: (" . $conex->errno . ") " . $conex->error . "</h5>";
 
−
                    if ($conex->errno==1451){  // Clave foránea relacionada
 
−
                        echo "<script>alert('O rexistro non se pode dar de baixa. Está sendo utilizado por outras táboas.');</script>";
 
−
                    }
 
−
                }
 
−
            }
 
−
         }  // Fin da operación de BAIXA
 
−
       
 
−
       
 
−
        // CARGAMOS AS DIFERENTES OPCIONS DE FORMULARIO EN BASE A OPERACION ESCOLLIDA DOS RADIOBUTTON: ALTA-BAIXA-MODIFICACION
 
−
        if (!empty($_POST['rbtnOperacion'])){  // Premimos unha opción => Xeramos o formulario
 
−
            switch ($_POST['rbtnOperacion']){
 
−
                case 'ALTA':
 
−
                    printf("<div>Nome do novo estado:");
 
−
                    printf("<input name='txtEstado' type='text' size='50' maxlength='100' /></div>");
 
−
                    printf("<div><input name='btnOperacion' type='submit' value='ALTA' />");
 
−
                    printf("<input type='reset' value='BORRAR' /></div>");
 
−
                    break;
 
−
                case 'BAIXA':
 
−
                    printf("<div>Lista de estados:");
 
−
                    $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_ESTADOS);
 
−
                    printf("<select name='lstEstados'>");
 
−
                    if ($conex->multi_query($proc)){
 
−
                        $result = $conex->store_result();
 
−
                        if ($result && ($result->num_rows>0)){
 
−
                            while($fila=$result->fetch_assoc()){
 
−
                                printf("<option value='%d'>%s</option>",$fila['id_estado'],$fila['descripcion']);
 
−
                            }
 
−
                            $result->free();
 
−
                        }
 
−
                        while($conex->next_result() && $conex->store_result());
 
−
                    }
 
−
                    printf("</select>");
 
−
                    printf("</div>");
 
−
                    printf("<div><input name='btnOperacion' type='submit' value='BAIXA' />");
 
−
                    break;
 
−
            }
 
−
        }
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
        <?php
 
−
        // Xestionamos a operacion do segundo formulario => Alta
 
−
        if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion']=='ALTA'){ 
 
−
            if (strlen($_POST['txtEstado'])>100){
 
−
                $erro = ">A lonxitude da cadea non pode ter máis de 100 caracteres...";
 
−
            }
 
−
            else {
 
−
                // Eliminamos caracteres non permitidos
 
−
                $novoEstado = filter_var($_POST['txtEstado'],FILTER_SANITIZE_STRING);
 
−
                // Escapamos as comillas simples e dobres
 
−
                $novoEstado = $conex->real_escape_string($novoEstado);
 
−
                $proc=sprintf(PROCEDEMENTO_INSERIR_ESTADO,$novoEstado);
 
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();
 
−
                    if ($result && ($result->num_rows>0)){
 
−
                        $fila=$result->fetch_assoc();
 
−
                        switch($fila['valor_devolto']){
 
−
                            case -1:
 
−
                                $erro = "Ese valor xa est&aacute; dado de alta na base de datos...";
 
−
                                break;
 
−
                            default:
 
−
                                printf("<h2>Valor dado de alta correctamente</h2>");
 
−
                                printf("<h3>Nova clave %d</h3>",$fila['valor_devolto']);
 
−
                               
 
−
                                break;
 
−
                        }
 
−
                        $result->free();
 
−
                    }
 
−
                    else{  // Sempre ten que devolver algo como temos implementado o proc.
 
−
                        $erro = 'Houbo un erro na chamada á base de datos';
 
−
                    }
 
−
                    while($conex->next_result() && $conex->store_result());
 
−
                }
 
−
                else{
 
−
                    $erro = 'Houbo un erro na chamada á base de datos';
 
−
                }
 
−
            } 
 
−
               
 
−
        }  // Fin da operacion de ALTA
 
−
       
 
−
       
 
−
       
 
−
        ?>
 
−
        <?php       
 
−
        if ($erro!='') {
 
−
            printf("<h3>%s</h3>",$erro);
 
−
        }
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
 +
    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 283: 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]] -- (2023).
+
<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).