Diferencia entre revisiones de «PHP UD3 OP MODIFICAR»

De MediaWiki
Ir a la navegación Ir a la búsqueda
Línea 1: Línea 1:
−
===Introducción===
+
=== Introdución ===
  
 +
Nesta unidade veremos como realizar operacións de modificación 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>.
 +
:* Para os exemplos que traballan con cadeas UTF-8 empregaremos tamén a extensión <code>mbstring</code>.
 +
:* Os exemplos están pensados para '''PHP 8.5'''.
 +
:* Partiremos do código realizado anteriormente nas seccións de [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA alta] e baixa.
  
 +
* '''NOTA IMPORTANTE:''' cando empregamos datos procedentes dun formulario debemos validalos no servidor e utilizar consultas preparadas para incorporalos ás ordes SQL.
  
−
* '''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.
+
: 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.
  
 +
<br />
 +
 +
==== Organización dos exemplos ====
 +
 +
Nos exemplos completos empregaremos o mesmo esquema que nas seccións anteriores.
 +
 +
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 que se vaian empregar:
 +
 +
::<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 />
 
<br />
  
 +
=== Modificación de rexistros: UPDATE ===
  
 +
* A modificación de datos realízase mediante a orde SQL <code>UPDATE</code>.
  
−
* A modificación se produce cando executamos a [http://www.w3schools.com/sql/sql_update.asp orde SQL UPDATE].
+
Por exemplo:
  
−
* Ao igual que no caso INSERT, se modificamos as claves primarias, farase o action recollido na creación das táboas.
+
::<syntaxhighlight lang="sql" enclose="div">
−
: Normalmente non se debe modificar a clave primaria das táboas e o que modificaremos será algún dos atributos da táboa.
+
UPDATE estados
 +
SET descripcion = 'NOVO VALOR'
 +
WHERE id = 10;
 +
</syntaxhighlight>
  
 +
* A orde <code>UPDATE</code> indicará:
  
−
* A orde SQL UPDATE levará como parámetros os novos valores a modificar e enviarase a clave primaria para que poida buscar a fila concreta que vai modificarse.
+
:* a táboa que queremos modificar;
 +
:* as columnas e os novos valores mediante <code>SET</code>;
 +
:* as filas que se deben modificar mediante <code>WHERE</code>.
  
−
* Ao igual que no caso da orde INSERT, podemos saber o número de rexistros afectados chamando á [http://php.net/manual/es/mysqli.affected-rows.php función mysqi_affected_rows].
+
* '''IMPORTANTE:''' debemos ter especial coidado coa cláusula <code>WHERE</code>.
−
: Neste exercicio dita función debería devolver sempre 1, pero podería aplicarse a outros exemplo no que queremos informar ao usuario de cantos rexistros foron afectados pola opción.
 
  
 +
A seguinte orde:
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
UPDATE estados
 +
SET descripcion = 'NOVO VALOR';
 +
</syntaxhighlight>
  
−
* <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).
+
modificaría '''todas as filas''' da táboa.
  
 +
* Normalmente non modificaremos a clave primaria. O habitual é empregar a clave primaria para identificar a fila e modificar un ou varios dos seus atributos.
  
 +
* Se unha clave que se modifica está referenciada mediante unha clave foránea, o comportamento dependerá da acción definida na relación, por exemplo <code>ON UPDATE CASCADE</code> ou <code>ON UPDATE RESTRICT</code>.
  
 +
<br />
  
−
=== Caso Práctico: Modificación ===
+
==== UPDATE empregando PDO ====
  
 +
Sempre que a orde conteña valores variables empregaremos unha consulta preparada.
  
 +
Por exemplo:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'UPDATE estados
 +
    SET descripcion = :descripcion
 +
    WHERE id = :id_estado'
 +
);
 +
 +
$stmt->execute([
 +
    ':descripcion' => $descripcion,
 +
    ':id_estado' => $id
 +
]);
 +
</syntaxhighlight>
 +
 +
Non construiremos a orde SQL mediante concatenación nin empregando <code>sprintf()</code> para introducir os datos.
  
 
<br />
 
<br />
−
====Orientado a procedementos====
+
 
−
* Imos implementar esta opción na páxina '''consultar_estados.php''' continuando có visto [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA na sección de ALTA].
+
==== Número de filas afectadas ====
−
: Se estamos a utilizar procedementos almacenados, lembrar que en caso de erro '2014 - Commands out of sync; you can't run this command now', debemos engadir a seguinte liña despois de chamar a un procedemento almacenado:
+
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="9" >
+
Despois dun <code>UPDATE</code> podemos empregar:
−
                    if ($conex->multi_query($proc)){
+
 
−
                        $result = $conex->store_result();
+
::<syntaxhighlight lang="php" enclose="div">
−
                        if ($result && ($result->num_rows>0)){
+
$numRexistros = $stmt->rowCount();
−
                            while($fila=$result->fetch_assoc()){
 
−
                                // Procesamos cada fila
 
−
                            }
 
−
                            $result->free();
 
−
                        }
 
−
                        while($conex->next_result() && $conex->store_result());
 
−
                    }
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
para coñecer o número de filas afectadas.
  
 +
No caso dunha modificación realizada a partir dunha clave primaria normalmente obteremos:
  
−
* O que temos que facer é:
+
:* <code>1</code>: unha fila foi modificada.
−
:* Definir no arquivo bd.php unha constante coa orde SQL para modificar o nome dun estado a partires dun id dado (clave primaria)
+
:* <code>0</code>: ningunha fila foi modificada.
−
:* Modificar a páxina consultar_estados.php e engadir a opción 'Editar' a cada un dos estados. Debemos ir a unha páxina editar_estado.php.  
 
−
:: O poderemos facer:
 
−
:* Cun enlace href, enviando como parámetro o id do estado a modificar. Eu vou escoller esta opción, pero sodes libres de practicar e intentalo coa outra opción.
 
−
:* Cun formulario e un campo oculto có id do estado a modificar.
 
−
:* A páxina editar_estado.php terá un formulario cun action a 'procesar_editar_estado.php'. Dentro do formulario fará un include do arquivo campos_estado.php que xa foi feito na páxina de alta de estados.
 
−
:: Tamén obterá os datos do estado a partires do id enviado polo enlace href ou polo formulario da páxina consultar_estados.php. Ditos datos serán gardados en variables e o nome será empregado pola páxina campos_estado.php.
 
−
:: Modificaremos a páxina campos_estado.php para que teña en conta os datos obtidos da consulta á base de datos.
 
−
$e->getCode():* Crear a páxina procesar_borrar_estado.php que dará de baixa o estado e se se produce algún erro deberá de informar á páxina de consultar_estados.php de cal foi o erro. Ao igual que na alta, poderemos enviar dita información nun parámetro na URL de redirección ou veremos nas seguintes seccións como empregar variables de sesión.
 
−
:: A xestión do erro en caso de querer borrar un rexistro relacionado non a imos facer.
 
−
:: Lembrar que dito control poderíamos facelo no propio procedemento almacenado/páxina php, capturando a excepción ou comprobando cun select previo se existe o rexistro relacionado.
 
  
 +
'''IMPORTANTE:''' en MySQL un resultado de <code>0</code> non significa necesariamente que o identificador non exista.
  
 +
Por exemplo, se a fila xa contén exactamente o mesmo valor que intentamos gardar, a orde pode executarse correctamente pero non producir ningún cambio.
  
 +
Polo tanto, non debemos interpretar automaticamente:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt->rowCount() === 0
 +
</syntaxhighlight>
  
 +
como un erro.
  
 +
<br />
  
 +
=== Caso práctico: modificación dun estado ===
  
 +
Imos continuar o mantemento da táboa <code>ESTADOS</code> realizado nas seccións anteriores.
 +
 +
O proceso será:
 +
 +
:# Desde <code>consultar_estados.php</code> seleccionamos a opción '''Editar'''.
 +
:# Enviamos o identificador do estado a <code>editar_estado.php</code>.
 +
:# <code>editar_estado.php</code> consulta os datos actuais da fila.
 +
:# Amosamos un formulario cos valores actuais.
 +
:# O formulario envía os novos datos mediante <code>POST</code> a <code>procesar_editar_estado.php</code>.
 +
:# Validamos novamente todos os datos no servidor.
 +
:# Executamos un <code>UPDATE</code> mediante unha consulta preparada.
 +
:# Rediriximos novamente a <code>consultar_estados.php</code>.
 +
 +
Neste caso é correcto empregar unha petición <code>GET</code> para abrir a páxina de edición:
 +
 +
::<syntaxhighlight lang="text" enclose="div">
 +
editar_estado.php?id=5
 +
</syntaxhighlight>
 +
 +
porque esa petición unicamente '''consulta e amosa''' os datos.
 +
 +
A modificación real realizarase posteriormente mediante unha petición <code>POST</code>.
  
 
<br />
 
<br />
−
'''Posible solución Caso Práctico: Modificación'''
 
−
'''Arquivo: bd.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define('CONSULTAR_ESTADO_POR_ID',"SELECT id,descripcion FROM estados WHERE id=%d");
 
−
define('ACTUALIZAR_ESTADO',"UPDATE estados set descripcion='%s' WHERE id=%d");
 
−
</syntaxhighlight>
 
  
 +
==== Arquivo /var/www/confBD/bd.php ====
  
 +
Engadimos as seguintes consultas:
  
−
'''Arquivo: consultar_estados.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" enclose="div" highlight="39" >
+
define(
 +
    'CONSULTAR_ESTADO_POR_ID',
 +
    'SELECT id, descripcion
 +
    FROM estados
 +
    WHERE id = :id_estado'
 +
);
 +
 
 +
define(
 +
    'ACTUALIZAR_ESTADO',
 +
    'UPDATE estados
 +
    SET descripcion = :descripcion
 +
    WHERE id = :id_estado'
 +
);
 +
</syntaxhighlight>
 +
 
 +
Fixarse en que tanto o identificador como a nova descrición se envían mediante parámetros.
 +
 
 +
<br />
 +
 
 +
==== Modificación de consultar_estados.php ====
 +
 
 +
Engadimos a opción '''Editar''' na columna de accións.
 +
 
 +
Podemos manter a consulta e a estrutura realizadas nas seccións anteriores:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
  
−
try {
+
    $dsn = sprintf(
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
     $conex->set_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 PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<!DOCTYPE html>
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
<html lang="gl">
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
<head>
−
    <head>
+
    <meta charset="UTF-8">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
    <title>Xestión de estados</title>
−
        <title>Xestión de estados</title>
+
</head>
−
    </head>
+
<body>
−
     <body>
+
 
−
        <form action="alta_estado.php" method="post">
+
     <h1>Xestión de estados</h1>
−
            <button type="submit">Novo Estado</button>
+
 
−
        </form>
+
    <form action="alta_estado.php" method="get">
−
         <table border="1" style="margin-top:20px">
+
        <button type="submit">Novo estado</button>
 +
    </form>
 +
 
 +
    <table>
 +
         <thead>
 
             <tr>
 
             <tr>
−
                 <th>Estado</th>
+
                 <th>Id</th>
 +
                <th>Descrición</th>
 
                 <th>Accións</th>
 
                 <th>Accións</th>
 
             </tr>
 
             </tr>
−
         <?php
+
         </thead>
 +
 
 +
        <tbody>
  
−
            $resultado = $conex->query(CONSULTAR_ESTADOS);
+
        <?php foreach ($estados as $estado): ?>
  
−
             while($fila = $resultado->fetch_assoc()) {
+
             <tr>
−
                $nombre = htmlentities($fila['descripcion']);
 
−
                echo "<tr>";
 
−
                echo "<td>$nombre</td>";
 
−
                echo "<td>";
 
−
                printf("<td><a href='editar_estado.php?id=%d'>Editar</a>",urlencode($fila['id']));
 
−
                printf("<form action='procesar_borrar_estado.php' method='post'>");
 
−
                printf("<input type='hidden' name='id' value='%s' />",htmlentities($fila['id']));
 
−
                echo '<input onclick="return confirm(\'Estás seguro de que queres borrar este estado?\');" type="submit" value="Borrar" name="btnBorrarEstado" style="background:none;border:none;padding:0;maring:0;color: blue; text-decoration:underline; cursor:pointer; font-size:inherit" />';
 
−
                echo '</form>';
 
−
                echo "</td>";
 
  
−
                 echo "</tr>";
+
                 <td>
−
            }
+
                    <?= htmlspecialchars(
 +
                        (string) $estado['id'],
 +
                        ENT_QUOTES | ENT_SUBSTITUTE,
 +
                        'UTF-8'
 +
                    ) ?>
 +
                </td>
  
−
            $resultado->free();
+
                <td>
−
        ?>  
+
                    <?= htmlspecialchars(
−
        </table>
+
                        $estado['descripcion'],
 +
                        ENT_QUOTES | ENT_SUBSTITUTE,
 +
                        'UTF-8'
 +
                    ) ?>
 +
                </td>
  
 +
                <td>
  
−
        <div>
+
                    <a href="editar_estado.php?id=<?= urlencode(
−
            <?php
+
                        (string) $estado['id']
−
                if(isset($_GET['id'])) printf("<h3>Estado con id %s dado de alta correctamente.</h3>",htmlentities($_GET['id']));
+
                    ) ?>">
−
                if(isset($_GET['msg'])) printf("<h4>%s</h3>",htmlentities($_GET['msg']));
+
                        Editar
−
                if(isset($_GET['error'])) printf("<h2>Houbo un erro: %s</h2>",htmlentities($_GET['error']));
+
                    </a>
−
             ?>
+
 
−
         </div>  
+
                    <form
−
       
+
                        action="procesar_borrar_estado.php"
−
     </body>  
+
                        method="post"
 +
                        style="display:inline"
 +
                        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['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>
 
</html>
−
<?php
+
</syntaxhighlight>
−
}  // Pecha o try da parte superior da páxina
+
 
−
catch (mysqli_sql_exception $e) {
+
Aquí empregamos <code>urlencode()</code> para construír o valor do parámetro da URL.
−
     die("Erro mysql: " . $e->getMessage());
+
 
−
}
+
Iso non substitúe a validación no servidor: <code>editar_estado.php</code> debe comprobar igualmente que o identificador recibido é válido.
−
catch (Expcetion $e) {
+
 
−
     die("Erro xeral: " . $e->getMessage());
+
<br />
−
}
+
 
−
finally{
+
==== Modificación de campos_estado.php ====
−
  if(!empty($conex)){
+
 
−
      $conex->close();
+
O arquivo <code>campos_estado.php</code> xa se utilizaba na operación de alta.
−
  }
+
 
−
}
+
Modificámolo para que poida utilizarse tanto na alta como na edición:
−
?>
+
 
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php" enclose="div">
 +
<label for="descripcion">Descrición:</label>
 +
 
 +
<input
 +
    type="text"
 +
     id="descripcion"
 +
    name="descripcion"
 +
    minlength="4"
 +
     maxlength="100"
 +
    required
 +
    value="<?= htmlspecialchars(
 +
        $descripcion ?? '',
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    ) ?>"
 +
>
 +
</syntaxhighlight>
 +
 
 +
Se a variable <code>$descripcion</code> non existe, como sucederá inicialmente na páxina de alta, empregamos unha cadea baleira:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$descripcion ?? ''
 +
</syntaxhighlight>
  
 +
Se estamos editando unha fila, <code>$descripcion</code> conterá o valor recuperado da base de datos.
  
 +
'''IMPORTANTE:''' o dato gárdase na base de datos na súa forma normal. Aplicamos <code>htmlspecialchars()</code> neste momento porque estamos introducindo o valor dentro dun documento HTML.
  
 +
Non debemos gardar previamente na base de datos o resultado de <code>htmlspecialchars()</code> ou <code>htmlentities()</code>.
  
−
'''Arquivo: campos_estado.php'''
+
<br />
−
::<syntaxhighlight lang="java" enclose="div" highlight="2" >
 
−
<label for="nombre">Descricción:</label>
 
−
<input type="text" maxlength="100" id="descripcion" name="descripcion" required value="<?php echo isset($descripcion) ? htmlentities($descripcion) : ''; ?>"><br>
 
−
</syntaxhighlight>  
 
  
 +
==== Arquivo editar_estado.php ====
  
 +
Esta páxina recibe o identificador mediante <code>GET</code>, valida o seu formato, recupera os datos e amosa o formulario.
  
−
'''Arquivo: editar_estado.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
try {
−
$conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
+
 
−
$conex->set_charset('utf8mb4');
+
    require '/var/www/confBD/bd.php';
−
$erros = '';
 
  
−
?>
+
    $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
 +
        ]
 +
    );
 +
 
 +
    /*
 +
    * Validamos o identificador recibido na URL.
 +
    */
 +
    $id = filter_input(
 +
        INPUT_GET,
 +
        'id',
 +
        FILTER_VALIDATE_INT,
 +
        [
 +
            'options' => [
 +
                'min_range' => 1
 +
            ]
 +
        ]
 +
    );
  
−
<?php
+
     if ($id === false || $id === null) {
−
     $consulta = sprintf(CONSULTAR_ESTADO_POR_ID,$conex->real_escape_string($_GET['id']));
 
−
    $result=$conex->query($consulta);  // Ven na URL da paxina listar_pistas.php
 
−
    if ($result && $result->num_rows>0){
 
−
        $row = mysqli_fetch_assoc($result); // Os datos serán empregados polos inputs do formulario
 
−
        $descripcion = htmlentities($row['descripcion']);
 
−
        $id = htmlentities($row['id']);  //O id non se amosa ao usuario
 
  
−
         $result->free();
+
         header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode('O identificador non é válido.')
 +
        );
 +
        exit;
 
     }
 
     }
−
     else {
+
 
−
         $erros = "Non se atopan os datos da pista!!!!";
+
    /*
 +
    * Recuperamos os datos actuais do estado.
 +
    */
 +
    $stmt = $conex->prepare(
 +
        CONSULTAR_ESTADO_POR_ID
 +
    );
 +
 
 +
    $stmt->execute([
 +
        ':id_estado' => $id
 +
     ]);
 +
 
 +
    $estado = $stmt->fetch();
 +
 
 +
    if ($estado === false) {
 +
 
 +
         header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode('Non se atopou o estado indicado.')
 +
        );
 +
        exit;
 
     }
 
     }
  
 +
    /*
 +
    * Estes valores aínda non se codifican para HTML.
 +
    * O escapado farase no momento de xerar a saída.
 +
    */
 +
    $id = (int) $estado['id'];
 +
    $descripcion = $estado['descripcion'];
  
 +
} 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>
 
<!DOCTYPE html>
−
<html>
+
<html lang="gl">
 
<head>
 
<head>
−
  <title>Modificar Estado</title>
+
    <meta charset="UTF-8">
 +
    <title>Modificar estado</title>
 
</head>
 
</head>
−
<body>
+
<body>
−
     <h3>EDITAR ESTADO</h3>
+
 
 +
    <h1>Modificar estado</h1>
 +
 
 +
    <form
 +
        method="post"
 +
        action="procesar_editar_estado.php"
 +
     >
 +
 
 +
        <?php require 'campos_estado.php'; ?>
 +
 
 +
        <input
 +
            type="hidden"
 +
            name="id_orixinal"
 +
            value="<?= htmlspecialchars(
 +
                (string) $id,
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            ) ?>"
 +
        >
 +
 
 +
        <button
 +
            type="submit"
 +
            name="btnModificarEstado"
 +
            value="MODIFICAR"
 +
        >
 +
            Modificar
 +
        </button>
 +
 
 +
    </form>
  
−
  <form method="POST" action="procesar_editar_estado.php">
 
−
    <?php include 'campos_estado.php'; ?>
 
−
    <div>
 
−
        <input type="hidden" name="id_orixinal" value="<?php echo $id; ?>">
 
−
        <input type="submit" value="Modificar" name="btnModificarEstado">
 
−
    </div>
 
−
  </form>
 
−
  <?php
 
−
    if(!empty($erros)){
 
−
        printf("<h3>$erros</h3>");
 
−
    }
 
−
  ?>
 
 
</body>
 
</body>
 
</html>
 
</html>
 +
</syntaxhighlight>
 +
 +
<br />
 +
 +
==== Por que volvemos enviar o identificador? ====
 +
 +
A páxina <code>editar_estado.php</code> recibe o identificador mediante:
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="text" enclose="div">
 +
editar_estado.php?id=5
 +
</syntaxhighlight>
  
 +
pero cando enviamos o formulario a:
  
 +
::<syntaxhighlight lang="text" enclose="div">
 +
procesar_editar_estado.php
 +
</syntaxhighlight>
  
−
'''Arquivo: procesar_editar_estado.php'''
+
necesitamos indicar que fila debe modificarse.
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
 +
Por iso incluímos:
 +
 
 +
::<syntaxhighlight lang="html" enclose="div">
 +
<input
 +
    type="hidden"
 +
    name="id_orixinal"
 +
    value="5"
 +
>
 +
</syntaxhighlight>
 +
 
 +
'''IMPORTANTE:''' que un campo sexa <code>hidden</code> non significa que sexa seguro.
 +
 
 +
O usuario pode modificar manualmente o valor antes de enviar a petición. Por iso <code>procesar_editar_estado.php</code> debe validalo novamente.
 +
 
 +
Nunha aplicación con control de usuarios tamén deberemos comprobar que o usuario autenticado ten autorización para modificar a fila indicada.
 +
 
 +
<br />
 +
 
 +
==== Arquivo procesar_editar_estado.php ====
 +
 
 +
Esta páxina recibe os datos mediante <code>POST</code>, valida os valores e realiza o <code>UPDATE</code>.
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
  
−
$conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
+
    $dsn = sprintf(
−
$conex->set_charset('utf8mb4');
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
$error = '';
+
    $conex = new PDO(
−
$msg = '';
+
        $dsn,
−
if (isset($_POST['btnModificarEstado'])) {
+
        USUARIO,
−
  try {
+
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
     // Lóxica para dar de alta unha nova pista
+
     /*
−
     $desc_novo = $conex->real_escape_string(strtoupper($_POST['descripcion']));
+
    * Comprobamos que se chegou desde
−
    if(strlen($desc_novo)<4 || strlen($desc_novo)>100){
+
    * a operación prevista.
−
      $error = sprintf("error=%s", urlencode("O nome do estado non pode ter menos de 4 ou 100 caracteres"));
+
    */
 +
     if (
 +
        !isset($_POST['btnModificarEstado']) ||
 +
        $_POST['btnModificarEstado'] !== 'MODIFICAR'
 +
    ) {
 +
        throw new RuntimeException(
 +
            'Operación non válida.'
 +
        );
 
     }
 
     }
−
     else{
+
 
−
      $id = $conex->real_escape_string($_POST['id_orixinal']); // Ven nun campo oculto do form
+
     /*
−
      $consulta = sprintf(ACTUALIZAR_ESTADO,$desc_novo, $id);
+
    * Validamos o identificador.
−
      $conex->query($consulta);
+
    */
−
 
+
    $id = filter_input(
−
      $msg = sprintf("msg=%s", urlencode("Estado actualizado correctamente."));
+
        INPUT_POST,
−
 
+
        'id_orixinal',
 +
        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;
 
     }
 
     }
−
  }
+
 
−
  catch (mysqli_sql_exception $e) {
+
    /*
−
      $error=sprintf("error='Erro mysql: %s'",urlencode($e->getMessage()));
+
    * Recuperamos e normalizamos a descrición.
−
  }
+
    */
−
  catch (Expcetion $e) {
+
    $descripcion = trim(
−
      $error=sprintf("error='Erro xeral: %s'",urlencode($e->getMessage()));
+
        $_POST['descripcion'] ?? ''
−
  }
+
    );
−
  finally{
+
 
−
    if(!empty($conex)){
+
    $lonxitude = mb_strlen(
−
      $conex->close();
+
        $descripcion,
−
     } 
+
        'UTF-8'
−
  }
+
    );
 +
 
 +
    if (
 +
        $lonxitude < 4 ||
 +
        $lonxitude > 100
 +
    ) {
 +
 
 +
        header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode(
 +
                'A descrición debe ter entre 4 e 100 caracteres.'
 +
            )
 +
        );
 +
        exit;
 +
    }
 +
 
 +
    /*
 +
    * Normalización definida pola aplicación.
 +
    */
 +
    $descripcion = mb_strtoupper(
 +
        $descripcion,
 +
        'UTF-8'
 +
    );
 +
 
 +
    /*
 +
    * Executamos o UPDATE mediante
 +
    * unha consulta preparada.
 +
    */
 +
    $stmt = $conex->prepare(
 +
        ACTUALIZAR_ESTADO
 +
    );
 +
 
 +
    $stmt->execute([
 +
        ':descripcion' => $descripcion,
 +
        ':id_estado' => $id
 +
    ]);
 +
 
 +
    $numRexistros = $stmt->rowCount();
 +
 
 +
    /*
 +
    * En MySQL rowCount() pode devolver 0 se
 +
    * o valor novo é igual ao que xa estaba gardado.
 +
    */
 +
    if ($numRexistros === 1) {
 +
 
 +
        $mensaxe =
 +
            'Estado actualizado correctamente.';
 +
 
 +
    } else {
 +
 
 +
        $mensaxe =
 +
            'A operación realizouse, pero non foi necesario ' .
 +
            'modificar ningún dato.';
 +
    }
 +
 
 +
    header(
 +
        'Location: consultar_estados.php?msg=' .
 +
        urlencode($mensaxe)
 +
    );
 +
    exit;
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
 +
 
 +
    header(
 +
        'Location: consultar_estados.php?error=' .
 +
        urlencode('Erro relacionado coa base de datos.')
 +
    );
 +
    exit;
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    error_log($e->getMessage());
 +
 
 +
    header(
 +
        'Location: consultar_estados.php?error=' .
 +
        urlencode('Produciuse un erro na aplicación.')
 +
     );
 +
    exit;
 
}
 
}
−
$url = sprintf("consultar_estados.php?%s&%s",$msg,$error);
+
</syntaxhighlight>
−
header("Location:" . $url);
+
 
 +
<br />
 +
 
 +
==== Validación da descrición ====
 +
 
 +
Empregamos:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$descripcion = trim(
 +
    $_POST['descripcion'] ?? ''
 +
);
 +
</syntaxhighlight>
 +
 
 +
para eliminar espazos ao principio e ao final.
 +
 
 +
Despois comprobamos a lonxitude con:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$lonxitude = mb_strlen(
 +
    $descripcion,
 +
    'UTF-8'
 +
);
 +
</syntaxhighlight>
 +
 
 +
Empregamos <code>mb_strlen()</code> porque estamos traballando con texto UTF-8 e queremos contar correctamente caracteres como:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
á
 +
é
 +
í
 +
ó
 +
ú
 +
ñ
 +
</syntaxhighlight>
 +
 
 +
A validación HTML realizada no formulario mediante:
 +
 
 +
::<syntaxhighlight lang="html" enclose="div">
 +
minlength="4"
 +
maxlength="100"
 +
required
 +
</syntaxhighlight>
 +
 
 +
mellora a experiencia do usuario, pero '''non substitúe a validación no servidor'''.
 +
 
 +
<br />
 +
 
 +
==== Normalización a maiúsculas ====
 +
 
 +
Se a regra da aplicación indica que as descricións deben gardarse en maiúsculas, empregaremos:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$descripcion = mb_strtoupper(
 +
    $descripcion,
 +
    'UTF-8'
 +
);
 +
</syntaxhighlight>
 +
 
 +
Por exemplo:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
área pública
 +
</syntaxhighlight>
 +
 
 +
convértese en:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
ÁREA PÚBLICA
 +
</syntaxhighlight>
 +
 
 +
Esta operación é unha '''normalización dos datos'''. Non é unha medida para evitar SQL Injection.
 +
 
 +
A protección fronte a SQL Injection obtense empregando correctamente a consulta preparada.
 +
 
 +
<br />
 +
 
 +
==== Comportamento de rowCount() nun UPDATE ====
 +
 
 +
Consideremos que a táboa contén:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
id = 5
 +
descripcion = ACTIVO
 +
</syntaxhighlight>
 +
 
 +
Se executamos:
 +
 
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
UPDATE estados
 +
SET descripcion = 'INACTIVO'
 +
WHERE id = 5;
 +
</syntaxhighlight>
 +
 
 +
produciuse unha modificación e <code>rowCount()</code> normalmente devolverá:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
1
 +
</syntaxhighlight>
 +
 
 +
Pero se executamos:
 +
 
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
UPDATE estados
 +
SET descripcion = 'ACTIVO'
 +
WHERE id = 5;
 +
</syntaxhighlight>
 +
 
 +
cando o valor xa era <code>ACTIVO</code>, MySQL pode indicar:
  
−
?>
+
::<syntaxhighlight lang="text" enclose="div">
 +
0
 +
</syntaxhighlight>
 +
 
 +
filas modificadas.
  
−
</syntaxhighlight>  
+
Por iso un resultado de <code>0</code> non permite distinguir por si só entre:
  
 +
:* unha fila inexistente;
 +
:* unha fila existente cuxo valor xa era igual ao novo valor.
  
 +
Se a aplicación necesita distinguir ambos os casos, podemos comprobar previamente a existencia da fila mediante unha consulta adicional ou deseñar a lóxica da aplicación para facelo.
  
 
<br />
 
<br />
−
====Empregando PDO====
 
−
* Imos implementar esta opción na páxina '''consultar_estados.php''' continuando có visto [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_ALTA na sección de ALTA].
 
  
 +
=== Evitando valores duplicados ===
  
−
* O que temos que facer é:
+
Se a descrición dun estado debe ser única, a regra debería estar garantida tamén na base de datos.
−
:* Definir no arquivo bd.php unha constante coa orde SQL para modificar o nome dun estado a partires dun id dado (clave primaria)
 
−
:* Modificar a páxina consultar_estados.php e engadir a opción 'Editar' a cada un dos estados. Debemos ir a unha páxina editar_estado.php.
 
−
:: O poderemos facer:
 
−
:* Cun enlace href, enviando como parámetro o id do estado a modificar. Eu vou escoller esta opción, pero sodes libres de practicar e intentalo coa outra opción.
 
−
:* Cun formulario e un campo oculto có id do estado a modificar.
 
−
:* A páxina editar_estado.php terá un formulario cun action a 'procesar_editar_estado.php'. Dentro do formulario fará un include do arquivo campos_estado.php que xa foi feito na páxina de alta de estados.
 
−
:: Tamén obterá os datos do estado a partires do id enviado polo enlace href ou polo formulario da páxina consultar_estados.php. Ditos datos serán gardados en variables e o nome será empregado pola páxina campos_estado.php.
 
−
:: Modificaremos a páxina campos_estado.php para que teña en conta os datos obtidos da consulta á base de datos.
 
−
$e->getCode():* Crear a páxina procesar_borrar_estado.php que dará de baixa o estado e se se produce algún erro deberá de informar á páxina de consultar_estados.php de cal foi o erro. Ao igual que na alta, poderemos enviar dita información nun parámetro na URL de redirección ou veremos nas seguintes seccións como empregar variables de sesión.
 
−
:: A xestión do erro en caso de querer borrar un rexistro relacionado non a imos facer.
 
−
:: Lembrar que dito control poderíamos facelo no propio procedemento almacenado/páxina php, capturando a excepción ou comprobando cun select previo se existe o rexistro relacionado.
 
  
 +
Por exemplo:
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
ALTER TABLE estados
 +
ADD CONSTRAINT uq_estados_descripcion
 +
UNIQUE (descripcion);
 +
</syntaxhighlight>
  
 +
Deste xeito a base de datos impediría que unha modificación deixase dous estados coa mesma descrición.
  
 +
Se se viola unha restrición <code>UNIQUE</code>, MySQL pode devolver un erro de clave duplicada.
  
 +
PDO lanzará unha <code>PDOException</code>, que debe tratarse sen mostrar ao usuario directamente o contido de:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$e->getMessage()
 +
</syntaxhighlight>
  
 +
O detalle técnico debe rexistrarse:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
error_log($e->getMessage());
 +
</syntaxhighlight>
 +
 +
e ao usuario mostraráselle unha mensaxe axeitada.
  
 
<br />
 
<br />
−
'''Posible solución Caso Práctico: Modificación'''
 
−
'''Arquivo: bd.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define('CONSULTAR_ESTADO_POR_ID',"SELECT id,descripcion FROM estados WHERE id=:id_estado");
 
−
define('ACTUALIZAR_ESTADO',"UPDATE estados set descripcion=:descripcion WHERE id=:id_estado");
 
−
</syntaxhighlight>
 
  
 +
=== Modificación mediante un procedemento almacenado ===
  
 +
Tamén podemos realizar a modificación mediante un procedemento almacenado.
  
−
'''Arquivo: consultar_estados.php'''
+
Por exemplo:
−
::<syntaxhighlight lang="java" enclose="div" highlight="41" >
+
 
−
<?php
+
::<syntaxhighlight lang="sql" enclose="div">
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
DELIMITER $$
−
ini_set('display_errors', 1);
+
 
−
ini_set('display_startup_errors', 1);
+
CREATE PROCEDURE ACTUALIZAR_ESTADO_PROC(
−
error_reporting(E_ALL);
+
    IN p_id INT,
 +
    IN p_descripcion VARCHAR(100)
 +
)
 +
BEGIN
 +
 
 +
    UPDATE estados
 +
    SET descripcion = p_descripcion
 +
    WHERE id = p_id;
 +
 
 +
    SELECT ROW_COUNT() AS rexistros_modificados;
 +
 
 +
END$$
 +
 
 +
DELIMITER ;
 +
</syntaxhighlight>
 +
 
 +
No arquivo <code>bd.php</code> definimos:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'PROCEDEMENTO_ACTUALIZAR_ESTADO',
 +
    'CALL ACTUALIZAR_ESTADO_PROC(
 +
        :id_estado,
 +
        :descripcion
 +
    )'
 +
);
 +
</syntaxhighlight>
  
−
require('/var/www/confBD/bd.php');
+
E desde PHP:
  
−
try {
+
::<syntaxhighlight lang="php" enclose="div">
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
$stmt = $conex->prepare(
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
+
    PROCEDEMENTO_ACTUALIZAR_ESTADO
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
);
  
−
     $erro = "";   // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
$stmt->execute([
−
?>
+
     ':id_estado' => $id,
 +
    ':descripcion' => $descripcion
 +
]);
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
$fila = $stmt->fetch();
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Xestión de estados</title>
 
−
    </head>
 
−
    <body>
 
−
        <form action="alta_estado.php" method="post">
 
−
            <button type="submit">Novo Estado</button>
 
−
        </form>
 
−
        <table border="1" style="margin-top:20px">
 
−
            <tr>
 
−
                <th>Estado</th>
 
−
                <th>Accións</th>
 
−
            </tr>
 
−
        <?php
 
−
            $result=$conex->query(CONSULTAR_ESTADOS);    // Temos que poñer prepare
 
−
            if ($result && $result->rowCount() > 0) {
 
−
                while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
 
−
                    $nombre = htmlentities($row['descripcion']);
 
−
                    echo "<tr>";
 
−
                    echo "<td>$nombre</td>";
 
−
                    echo "<td>";
 
−
                    printf("<a href='editar_estado.php?id=%s'>Editar</a>",urlencode($row['id']));
 
−
                    echo("<form action='procesar_borrar_estado.php' method='post'>");
 
−
                    printf("<input type='hidden' name='id' value='%s' />",htmlentities($row['id']));
 
−
                    echo '<input onclick="return confirm(\'Estás seguro de que queres borrar este estado?\');" type="submit" value="Borrar" name="btnBorrarEstado" style="background:none;border:none;padding:0;margin:0;color: blue; text-decoration:underline; cursor:pointer; font-size:inherit" />';
 
−
                    echo '</form>';
 
−
                    echo "</td>";
 
−
                    echo "</tr>";
 
−
                }
 
−
            }
 
  
−
            $result = null;
+
$stmt->closeCursor();
−
        ?>   
 
−
        </table>
 
  
 +
$numRexistros =
 +
    (int) $fila['rexistros_modificados'];
 +
</syntaxhighlight>
  
−
        <div>
+
Neste caso:
−
            <?php
 
−
                if(isset($_GET['id'])) printf("<h3>Estado con id %s dado de alta correctamente.</h3>",htmlentities($_GET['id']));
 
−
                if(isset($_GET['msg'])) printf("<h4>%s</h3>",htmlentities($_GET['msg']));
 
−
                if(isset($_GET['error'])) printf("<h2>Houbo un erro: %s</h2>",htmlentities($_GET['error']));
 
−
            ?>
 
−
        </div>   
 
−
       
 
−
    </body>   
 
−
</html>
 
−
<?php
 
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
:* 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 ===
  
−
'''Arquivo: campos_estado.php'''
+
PDO será o mecanismo principal dos apuntes, pero podemos realizar a mesma operación empregando <code>mysqli</code>.
−
::<syntaxhighlight lang="java" enclose="div" highlight="2" >
 
−
<label for="nombre">Descricción:</label>
 
−
<input type="text" maxlength="100" id="descripcion" name="descripcion" required value="<?php echo isset($descripcion) ? htmlentities($descripcion) : ''; ?>">
 
−
</syntaxhighlight>  
 
  
 +
<br />
  
 +
==== Conexión con mysqli ====
  
−
'''Arquivo: editar_estado.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
+
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
  
−
try {
+
    mysqli_report(
−
     $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
        MYSQLI_REPORT_ERROR |
−
     $conex = new PDO($dsn, USUARIO, PASSWORD);
+
        MYSQLI_REPORT_STRICT
−
     $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
     );
 +
 
 +
     $conex = new mysqli(
 +
        SERVIDOR,
 +
        USUARIO,
 +
        PASSWORD,
 +
        BD
 +
    );
 +
 
 +
     $conex->set_charset('utf8mb4');
  
−
     $erro = "";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
     // Operacións coa base de datos...
  
−
    $stmt = $conex->prepare(CONSULTAR_ESTADO_POR_ID);
+
} catch (mysqli_sql_exception $e) {
−
    $stmt->bindValue(":id_estado", $_GET['id'], PDO::PARAM_INT);
 
−
    $result = $stmt->execute();
 
−
    if ($result) {
 
−
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
 
−
        $id = $row['id'];
 
−
        $descripcion = htmlentities($row['descripcion']);
 
  
−
        $stmt = null;
+
     error_log($e->getMessage());
−
     } else {
 
−
        $erros = "Non se atopan os datos da pista!!!!";
 
−
    }
 
  
 +
    die('Erro relacionado coa base de datos.');
  
−
?>
+
} catch (Throwable $e) {
−
<!DOCTYPE html>
 
−
<html>
 
−
<head>
 
−
  <title>Modificar Estado</title>
 
−
</head>
 
−
<body> 
 
−
    <h3>EDITAR ESTADO</h3>
 
  
−
  <form method="POST" action="procesar_editar_estado.php">
+
     error_log($e->getMessage());
−
     <?php include 'campos_estado.php'; ?>
 
−
    <div>
 
−
        <input type="hidden" name="id_orixinal" value="<?php echo $id; ?>">
 
−
        <input type="submit" value="Modificar" name="btnModificarEstado">
 
−
    </div>
 
−
  </form>
 
−
  <?php
 
−
    if(!empty($erros)){
 
−
        printf("<h3>$erros</h3>");
 
−
    }
 
−
  ?>
 
−
</body>
 
−
</html>
 
  
−
<?php
+
     die('Produciuse un erro na aplicación.');
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
     die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
 
}
 
}
−
?>
+
</syntaxhighlight>
−
</syntaxhighlight>  
+
 
 +
<br />
 +
 
 +
==== Consultas preparadas con mysqli ====
 +
 
 +
No arquivo <code>bd.php</code> poderiamos definir:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'CONSULTAR_ESTADO_POR_ID_MYSQLI',
 +
    'SELECT id, descripcion
 +
    FROM estados
 +
    WHERE id = ?'
 +
);
 +
 
 +
define(
 +
    'ACTUALIZAR_ESTADO_MYSQLI',
 +
    'UPDATE estados
 +
    SET descripcion = ?
 +
    WHERE id = ?'
 +
);
 +
</syntaxhighlight>
 +
 
 +
Para consultar unha fila:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    CONSULTAR_ESTADO_POR_ID_MYSQLI
 +
);
 +
 
 +
$stmt->bind_param(
 +
    'i',
 +
    $id
 +
);
 +
 
 +
$stmt->execute();
  
 +
$resultado = $stmt->get_result();
  
 +
$estado = $resultado->fetch_assoc();
 +
</syntaxhighlight>
  
−
'''Arquivo: procesar_editar_estado.php'''
+
Para modificar:
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
+
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    ACTUALIZAR_ESTADO_MYSQLI
 +
);
  
−
try {
+
$stmt->bind_param(
−
     $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
     'si',
−
     $conex = new PDO($dsn, USUARIO, PASSWORD);
+
     $descripcion,
−
     $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
     $id
 +
);
  
 +
$stmt->execute();
  
−
    $error = '';
+
$numRexistros = $stmt->affected_rows;
−
    $msg = '';
+
</syntaxhighlight>
−
    if (isset($_POST['btnModificarEstado'])) {
 
  
−
        $desc_novo = strtoupper($_POST['descripcion']);
+
Neste caso:
−
        if (strlen($desc_novo) < 4 || strlen($desc_novo) > 100) {
 
−
            $error = sprintf("error=%s", urlencode("O nome do estado non pode ter menos de 4 ou 100 caracteres"));
 
−
        } else {
 
−
            $stmt = $conex->prepare(ACTUALIZAR_ESTADO);    // Temos que poñer prepare
 
−
            $stmt->bindValue(':id_estado', $_POST['id_orixinal'], PDO::PARAM_INT);
 
−
            $stmt->bindValue(':descripcion', $desc_novo, PDO::PARAM_STR);
 
−
            $result = $stmt->execute();
 
  
−
            if ($result) {
+
:* <code>s</code> indica unha cadea.
−
                $msg = sprintf("msg=%s", urlencode("Estado actualizado correctamente."));
+
:* <code>i</code> indica un enteiro.
−
            } else {
 
−
                $error = sprintf("error=$s", urlencode("Houbo un problema na actualización."));
 
−
            }
 
−
        }
 
−
    }
 
−
} catch (PDOException $e) {
 
−
    $error = sprintf("error=PDO:%s", urlencode($e->getMessage()));
 
−
} catch (Exception $e) {
 
−
    $error = sprintf("error=ERRO XERAL:%s", urlencode($e->getMessage()));
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
$url = sprintf("consultar_estados.php?%s&%s", $msg, $error);
 
−
header("Location:" . $url);
 
  
−
</syntaxhighlight>
+
Non necesitamos empregar:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$conex->real_escape_string(...)
 +
</syntaxhighlight>
  
 +
nin construír a consulta con <code>sprintf()</code>, porque os valores se envían mediante unha consulta preparada.
  
 +
<br />
  
 +
=== Resumo ===
  
 +
* En PHP 8.5 empregaremos '''PDO como mecanismo principal''' para realizar modificacións.
 +
* As consultas gardaranse en <code>/var/www/confBD/bd.php</code>, fóra de <code>/var/www/html</code>.
 +
* Os valores variables dun <code>UPDATE</code> enviaranse mediante consultas preparadas.
 +
* Non construiremos a orde SQL concatenando ou interpolando datos recibidos do usuario.
 +
* Empregar <code>GET</code> para abrir o formulario de edición é correcto porque esa petición non modifica datos.
 +
* A modificación realizarase mediante <code>POST</code>.
 +
* 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 modificado polo usuario.
 +
* A validación dun identificador non substitúe as comprobacións de autorización.
 +
* Os datos recuperados da base de datos non se codifican antes de gardalos nunha variable.
 +
* Empregaremos <code>htmlspecialchars()</code> cando introduzamos os datos nun documento HTML.
 +
* Para texto UTF-8 empregaremos <code>mb_strlen()</code> e, cando sexa necesario normalizar a maiúsculas, <code>mb_strtoupper()</code>.
 +
* <code>PDOStatement::rowCount()</code> permite coñecer cantas filas foron modificadas por un <code>UPDATE</code>.
 +
* En MySQL, <code>rowCount() === 0</code> non implica necesariamente que a fila non exista: pode significar que o novo valor era igual ao xa gardado.
 +
* As regras de unicidade e integridade deben estar garantidas tamén pola base de datos.
 +
* Os detalles técnicos das excepcións deben gardarse no log e non mostrarse directamente aos usuarios.
 +
* <code>mysqli</code> explicarase como alternativa adicional, tamén empregando consultas preparadas.
  
−
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2025).
+
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2026).

Revisión del 10:19 22 sep 2026

Introdución

Nesta unidade veremos como realizar operacións de modificación 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.
  • Para os exemplos que traballan con cadeas UTF-8 empregaremos tamén a extensión mbstring.
  • Os exemplos están pensados para PHP 8.5.
  • Partiremos do código realizado anteriormente nas seccións de alta e baixa.
  • NOTA IMPORTANTE: cando empregamos datos procedentes dun formulario debemos validalos no servidor e utilizar consultas preparadas para incorporalos ás ordes SQL.
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.


Organización dos exemplos

Nos exemplos completos empregaremos o mesmo esquema que nas seccións anteriores.

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 que se vaian empregar:

<?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.');
}


Modificación de rexistros: UPDATE

  • A modificación de datos realízase mediante a orde SQL UPDATE.

Por exemplo:

UPDATE estados
SET descripcion = 'NOVO VALOR'
WHERE id = 10;
  • A orde UPDATE indicará:
  • a táboa que queremos modificar;
  • as columnas e os novos valores mediante SET;
  • as filas que se deben modificar mediante WHERE.
  • IMPORTANTE: debemos ter especial coidado coa cláusula WHERE.

A seguinte orde:

UPDATE estados
SET descripcion = 'NOVO VALOR';

modificaría todas as filas da táboa.

  • Normalmente non modificaremos a clave primaria. O habitual é empregar a clave primaria para identificar a fila e modificar un ou varios dos seus atributos.
  • Se unha clave que se modifica está referenciada mediante unha clave foránea, o comportamento dependerá da acción definida na relación, por exemplo ON UPDATE CASCADE ou ON UPDATE RESTRICT.


UPDATE empregando PDO

Sempre que a orde conteña valores variables empregaremos unha consulta preparada.

Por exemplo:

$stmt = $conex->prepare(
    'UPDATE estados
     SET descripcion = :descripcion
     WHERE id = :id_estado'
);

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

Non construiremos a orde SQL mediante concatenación nin empregando sprintf() para introducir os datos.


Número de filas afectadas

Despois dun UPDATE podemos empregar:

$numRexistros = $stmt->rowCount();

para coñecer o número de filas afectadas.

No caso dunha modificación realizada a partir dunha clave primaria normalmente obteremos:

  • 1: unha fila foi modificada.
  • 0: ningunha fila foi modificada.

IMPORTANTE: en MySQL un resultado de 0 non significa necesariamente que o identificador non exista.

Por exemplo, se a fila xa contén exactamente o mesmo valor que intentamos gardar, a orde pode executarse correctamente pero non producir ningún cambio.

Polo tanto, non debemos interpretar automaticamente:

$stmt->rowCount() === 0

como un erro.


Caso práctico: modificación dun estado

Imos continuar o mantemento da táboa ESTADOS realizado nas seccións anteriores.

O proceso será:

  1. Desde consultar_estados.php seleccionamos a opción Editar.
  2. Enviamos o identificador do estado a editar_estado.php.
  3. editar_estado.php consulta os datos actuais da fila.
  4. Amosamos un formulario cos valores actuais.
  5. O formulario envía os novos datos mediante POST a procesar_editar_estado.php.
  6. Validamos novamente todos os datos no servidor.
  7. Executamos un UPDATE mediante unha consulta preparada.
  8. Rediriximos novamente a consultar_estados.php.

Neste caso é correcto empregar unha petición GET para abrir a páxina de edición:

editar_estado.php?id=5

porque esa petición unicamente consulta e amosa os datos.

A modificación real realizarase posteriormente mediante unha petición POST.


Arquivo /var/www/confBD/bd.php

Engadimos as seguintes consultas:

define(
    'CONSULTAR_ESTADO_POR_ID',
    'SELECT id, descripcion
     FROM estados
     WHERE id = :id_estado'
);

define(
    'ACTUALIZAR_ESTADO',
    'UPDATE estados
     SET descripcion = :descripcion
     WHERE id = :id_estado'
);

Fixarse en que tanto o identificador como a nova descrición se envían mediante parámetros.


Modificación de consultar_estados.php

Engadimos a opción Editar na columna de accións.

Podemos manter a consulta e a estrutura realizadas nas seccións anteriores:

<?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>

                    <a href="editar_estado.php?id=<?= urlencode(
                        (string) $estado['id']
                    ) ?>">
                        Editar
                    </a>

                    <form
                        action="procesar_borrar_estado.php"
                        method="post"
                        style="display:inline"
                        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['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>

Aquí empregamos urlencode() para construír o valor do parámetro da URL.

Iso non substitúe a validación no servidor: editar_estado.php debe comprobar igualmente que o identificador recibido é válido.


Modificación de campos_estado.php

O arquivo campos_estado.php xa se utilizaba na operación de alta.

Modificámolo para que poida utilizarse tanto na alta como na edición:

<label for="descripcion">Descrición:</label>

<input
    type="text"
    id="descripcion"
    name="descripcion"
    minlength="4"
    maxlength="100"
    required
    value="<?= htmlspecialchars(
        $descripcion ?? '',
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    ) ?>"
>

Se a variable $descripcion non existe, como sucederá inicialmente na páxina de alta, empregamos unha cadea baleira:

$descripcion ?? ''

Se estamos editando unha fila, $descripcion conterá o valor recuperado da base de datos.

IMPORTANTE: o dato gárdase na base de datos na súa forma normal. Aplicamos htmlspecialchars() neste momento porque estamos introducindo o valor dentro dun documento HTML.

Non debemos gardar previamente na base de datos o resultado de htmlspecialchars() ou htmlentities().


Arquivo editar_estado.php

Esta páxina recibe o identificador mediante GET, valida o seu formato, recupera os datos e amosa o formulario.

<?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
        ]
    );

    /*
     * Validamos o identificador recibido na URL.
     */
    $id = filter_input(
        INPUT_GET,
        '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;
    }

    /*
     * Recuperamos os datos actuais do estado.
     */
    $stmt = $conex->prepare(
        CONSULTAR_ESTADO_POR_ID
    );

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

    $estado = $stmt->fetch();

    if ($estado === false) {

        header(
            'Location: consultar_estados.php?error=' .
            urlencode('Non se atopou o estado indicado.')
        );
        exit;
    }

    /*
     * Estes valores aínda non se codifican para HTML.
     * O escapado farase no momento de xerar a saída.
     */
    $id = (int) $estado['id'];
    $descripcion = $estado['descripcion'];

} 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>Modificar estado</title>
</head>
<body>

    <h1>Modificar estado</h1>

    <form
        method="post"
        action="procesar_editar_estado.php"
    >

        <?php require 'campos_estado.php'; ?>

        <input
            type="hidden"
            name="id_orixinal"
            value="<?= htmlspecialchars(
                (string) $id,
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            ) ?>"
        >

        <button
            type="submit"
            name="btnModificarEstado"
            value="MODIFICAR"
        >
            Modificar
        </button>

    </form>

</body>
</html>


Por que volvemos enviar o identificador?

A páxina editar_estado.php recibe o identificador mediante:

editar_estado.php?id=5

pero cando enviamos o formulario a:

procesar_editar_estado.php

necesitamos indicar que fila debe modificarse.

Por iso incluímos:

<input
    type="hidden"
    name="id_orixinal"
    value="5"
>

IMPORTANTE: que un campo sexa hidden non significa que sexa seguro.

O usuario pode modificar manualmente o valor antes de enviar a petición. Por iso procesar_editar_estado.php debe validalo novamente.

Nunha aplicación con control de usuarios tamén deberemos comprobar que o usuario autenticado ten autorización para modificar a fila indicada.


Arquivo procesar_editar_estado.php

Esta páxina recibe os datos mediante POST, valida os valores e realiza o UPDATE.

<?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
        ]
    );

    /*
     * Comprobamos que se chegou desde
     * a operación prevista.
     */
    if (
        !isset($_POST['btnModificarEstado']) ||
        $_POST['btnModificarEstado'] !== 'MODIFICAR'
    ) {
        throw new RuntimeException(
            'Operación non válida.'
        );
    }

    /*
     * Validamos o identificador.
     */
    $id = filter_input(
        INPUT_POST,
        'id_orixinal',
        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;
    }

    /*
     * Recuperamos e normalizamos a descrición.
     */
    $descripcion = trim(
        $_POST['descripcion'] ?? ''
    );

    $lonxitude = mb_strlen(
        $descripcion,
        'UTF-8'
    );

    if (
        $lonxitude < 4 ||
        $lonxitude > 100
    ) {

        header(
            'Location: consultar_estados.php?error=' .
            urlencode(
                'A descrición debe ter entre 4 e 100 caracteres.'
            )
        );
        exit;
    }

    /*
     * Normalización definida pola aplicación.
     */
    $descripcion = mb_strtoupper(
        $descripcion,
        'UTF-8'
    );

    /*
     * Executamos o UPDATE mediante
     * unha consulta preparada.
     */
    $stmt = $conex->prepare(
        ACTUALIZAR_ESTADO
    );

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

    $numRexistros = $stmt->rowCount();

    /*
     * En MySQL rowCount() pode devolver 0 se
     * o valor novo é igual ao que xa estaba gardado.
     */
    if ($numRexistros === 1) {

        $mensaxe =
            'Estado actualizado correctamente.';

    } else {

        $mensaxe =
            'A operación realizouse, pero non foi necesario ' .
            'modificar ningún dato.';
    }

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

} catch (PDOException $e) {

    error_log($e->getMessage());

    header(
        'Location: consultar_estados.php?error=' .
        urlencode('Erro relacionado coa base de datos.')
    );
    exit;

} catch (Throwable $e) {

    error_log($e->getMessage());

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


Validación da descrición

Empregamos:

$descripcion = trim(
    $_POST['descripcion'] ?? ''
);

para eliminar espazos ao principio e ao final.

Despois comprobamos a lonxitude con:

$lonxitude = mb_strlen(
    $descripcion,
    'UTF-8'
);

Empregamos mb_strlen() porque estamos traballando con texto UTF-8 e queremos contar correctamente caracteres como:

á
é
í
ó
ú
ñ

A validación HTML realizada no formulario mediante:

minlength="4"
maxlength="100"
required

mellora a experiencia do usuario, pero non substitúe a validación no servidor.


Normalización a maiúsculas

Se a regra da aplicación indica que as descricións deben gardarse en maiúsculas, empregaremos:

$descripcion = mb_strtoupper(
    $descripcion,
    'UTF-8'
);

Por exemplo:

área pública

convértese en:

ÁREA PÚBLICA

Esta operación é unha normalización dos datos. Non é unha medida para evitar SQL Injection.

A protección fronte a SQL Injection obtense empregando correctamente a consulta preparada.


Comportamento de rowCount() nun UPDATE

Consideremos que a táboa contén:

id = 5
descripcion = ACTIVO

Se executamos:

UPDATE estados
SET descripcion = 'INACTIVO'
WHERE id = 5;

produciuse unha modificación e rowCount() normalmente devolverá:

1

Pero se executamos:

UPDATE estados
SET descripcion = 'ACTIVO'
WHERE id = 5;

cando o valor xa era ACTIVO, MySQL pode indicar:

0

filas modificadas.

Por iso un resultado de 0 non permite distinguir por si só entre:

  • unha fila inexistente;
  • unha fila existente cuxo valor xa era igual ao novo valor.

Se a aplicación necesita distinguir ambos os casos, podemos comprobar previamente a existencia da fila mediante unha consulta adicional ou deseñar a lóxica da aplicación para facelo.


Evitando valores duplicados

Se a descrición dun estado debe ser única, a regra debería estar garantida tamén na base de datos.

Por exemplo:

ALTER TABLE estados
ADD CONSTRAINT uq_estados_descripcion
UNIQUE (descripcion);

Deste xeito a base de datos impediría que unha modificación deixase dous estados coa mesma descrición.

Se se viola unha restrición UNIQUE, MySQL pode devolver un erro de clave duplicada.

PDO lanzará unha PDOException, que debe tratarse sen mostrar ao usuario directamente o contido de:

$e->getMessage()

O detalle técnico debe rexistrarse:

error_log($e->getMessage());

e ao usuario mostraráselle unha mensaxe axeitada.


Modificación mediante un procedemento almacenado

Tamén podemos realizar a modificación mediante un procedemento almacenado.

Por exemplo:

DELIMITER $$

CREATE PROCEDURE ACTUALIZAR_ESTADO_PROC(
    IN p_id INT,
    IN p_descripcion VARCHAR(100)
)
BEGIN

    UPDATE estados
    SET descripcion = p_descripcion
    WHERE id = p_id;

    SELECT ROW_COUNT() AS rexistros_modificados;

END$$

DELIMITER ;

No arquivo bd.php definimos:

define(
    'PROCEDEMENTO_ACTUALIZAR_ESTADO',
    'CALL ACTUALIZAR_ESTADO_PROC(
        :id_estado,
        :descripcion
    )'
);

E desde PHP:

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

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

$fila = $stmt->fetch();

$stmt->closeCursor();

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

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.');
}


Consultas preparadas con mysqli

No arquivo bd.php poderiamos definir:

define(
    'CONSULTAR_ESTADO_POR_ID_MYSQLI',
    'SELECT id, descripcion
     FROM estados
     WHERE id = ?'
);

define(
    'ACTUALIZAR_ESTADO_MYSQLI',
    'UPDATE estados
     SET descripcion = ?
     WHERE id = ?'
);

Para consultar unha fila:

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

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

$stmt->execute();

$resultado = $stmt->get_result();

$estado = $resultado->fetch_assoc();

Para modificar:

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

$stmt->bind_param(
    'si',
    $descripcion,
    $id
);

$stmt->execute();

$numRexistros = $stmt->affected_rows;

Neste caso:

  • s indica unha cadea.
  • i indica un enteiro.

Non necesitamos empregar:

$conex->real_escape_string(...)

nin construír a consulta con sprintf(), porque os valores se envían mediante unha consulta preparada.


Resumo

  • En PHP 8.5 empregaremos PDO como mecanismo principal para realizar modificacións.
  • As consultas gardaranse en /var/www/confBD/bd.php, fóra de /var/www/html.
  • Os valores variables dun UPDATE enviaranse mediante consultas preparadas.
  • Non construiremos a orde SQL concatenando ou interpolando datos recibidos do usuario.
  • Empregar GET para abrir o formulario de edición é correcto porque esa petición non modifica datos.
  • A modificación realizarase mediante POST.
  • Un formulario POST non substitúe a protección CSRF.
  • Os identificadores recibidos deben validarse no servidor.
  • Un campo hidden pode ser modificado polo usuario.
  • A validación dun identificador non substitúe as comprobacións de autorización.
  • Os datos recuperados da base de datos non se codifican antes de gardalos nunha variable.
  • Empregaremos htmlspecialchars() cando introduzamos os datos nun documento HTML.
  • Para texto UTF-8 empregaremos mb_strlen() e, cando sexa necesario normalizar a maiúsculas, mb_strtoupper().
  • PDOStatement::rowCount() permite coñecer cantas filas foron modificadas por un UPDATE.
  • En MySQL, rowCount() === 0 non implica necesariamente que a fila non exista: pode significar que o novo valor era igual ao xa gardado.
  • As regras de unicidade e integridade deben estar garantidas tamén pola base de datos.
  • 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).