Diferencia entre revisiones de «PHP UD3 OP MODIFICAR»

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 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.
  
  
 +
 +
 +
* '''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 ====
 +
 +
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 />
 +
 +
=== Modificación de rexistros: UPDATE ===
 +
 +
* A modificación de datos realízase mediante a orde SQL <code>UPDATE</code>.
 +
 +
Por exemplo:
 +
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
UPDATE estados
 +
SET descripcion = 'NOVO VALOR'
 +
WHERE id = 10;
 +
</syntaxhighlight>
 +
 +
* A orde <code>UPDATE</code> indicará:
 +
 +
:* 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>.
 +
 +
* '''IMPORTANTE:''' debemos ter especial coidado coa cláusula <code>WHERE</code>.
 +
 +
A seguinte orde:
 +
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
UPDATE estados
 +
SET descripcion = 'NOVO VALOR';
 +
</syntaxhighlight>
 +
 +
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 />
 +
 +
==== 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 />
 +
 +
==== Número de filas afectadas ====
 +
 +
Despois dun <code>UPDATE</code> podemos empregar:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$numRexistros = $stmt->rowCount();
 +
</syntaxhighlight>
 +
 +
para coñecer o número de filas afectadas.
 +
 +
No caso dunha modificación realizada a partir dunha clave primaria normalmente obteremos:
 +
 +
:* <code>1</code>: unha fila foi modificada.
 +
:* <code>0</code>: ningunha fila foi modificada.
 +
 +
'''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 />
 +
 +
==== Arquivo /var/www/confBD/bd.php ====
 +
 +
Engadimos as seguintes consultas:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
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
 +
 +
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>
 +
</syntaxhighlight>
 +
 +
Aquí empregamos <code>urlencode()</code> para construír o valor do parámetro da URL.
 +
 +
Iso non substitúe a validación no servidor: <code>editar_estado.php</code> debe comprobar igualmente que o identificador recibido é válido.
  
 
<br />
 
<br />
  
 +
==== Modificación de campos_estado.php ====
 +
 +
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 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>.
 +
 +
<br />
 +
 +
==== 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.
 +
 +
::<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
 +
        ]
 +
    );
 +
 +
    /*
 +
    * 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
 +
    ]);
  
−
* A modificación se produce cando executamos a [http://www.w3schools.com/sql/sql_update.asp orde SQL UPDATE].
+
    $estado = $stmt->fetch();
  
−
* Ao igual que no caso INSERT, se modificamos as claves primarias, farase o action recollido na creación das táboas.
+
    if ($estado === false) {
−
: Normalmente non se debe modificar a clave primaria das táboas e o que modificaremos será algún dos atributos da táboa.
 
  
 +
        header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode('Non se atopou o estado indicado.')
 +
        );
 +
        exit;
 +
    }
  
−
* 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.
+
    /*
 +
    * 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'];
  
−
* 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].
+
} catch (PDOException $e) {
−
: 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.
 
  
 +
    error_log($e->getMessage());
  
 +
    die('Erro relacionado coa base de datos.');
  
−
* <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).
+
} 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"
 +
    >
  
−
=== Caso Práctico: Modificación ===
+
        <?php require 'campos_estado.php'; ?>
  
−
* Seguindo có noso exemplo imos a rematar a páxina 'Xestionar_Estados_Proc.php' engadindo a funcionalidade para modificar os estados.
+
        <input
 +
            type="hidden"
 +
            name="id_orixinal"
 +
            value="<?= htmlspecialchars(
 +
                (string) $id,
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            ) ?>"
 +
        >
  
−
: Cando se seleccione esta opción aparecerá unha lista. Deberemos escoller un estado da lista e teremos unha caixa de texto para darlle o novo valor ao elemento seleccionado.
+
        <button
−
: Ao premer o botón 'Modificar' modificarase o estado do elemento seleccionado.
+
            type="submit"
 +
            name="btnModificarEstado"
 +
            value="MODIFICAR"
 +
        >
 +
            Modificar
 +
        </button>
  
−
: Comprobaremos antes de facer a modificación, que estea seleccionado un elemento da lista.
+
    </form>
  
 +
</body>
 +
</html>
 +
</syntaxhighlight>
  
 +
<br />
  
−
<u>Nota:</u> Neste curso non entramos en temas de 'presentación' e 'javascript/jquery'. Por iso a 'presentación' de cara a un usuario final non é a adecuada.
+
==== Por que volvemos enviar o identificador? ====
−
: Neste caso poderíamos implementar, con 'javascript', que o usuario teña que seleccionar un elemento da lista antes de premer o botón 'Modificar'.
 
−
: Tamén poderíamos implementar con jquery/javascript unha lista 'editable'.
 
  
 +
A páxina <code>editar_estado.php</code> recibe o identificador mediante:
  
 +
::<syntaxhighlight lang="text" enclose="div">
 +
editar_estado.php?id=5
 +
</syntaxhighlight>
  
−
'''Posible solución Caso Práctico: Modificación'''
+
pero cando enviamos o formulario a:
  
−
'''Procedemento almacenado de Mysql MODIFICAR_ESTADO'''
+
::<syntaxhighlight lang="text" enclose="div">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
procesar_editar_estado.php
 +
</syntaxhighlight>
 +
 
 +
necesitamos indicar que fila debe modificarse.
 +
 
 +
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
 +
 
 +
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'
 +
    ) {
 +
 
 +
        header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode('O identificador non é válido.')
 +
        );
 +
        exit;
 +
        /* Outra forma de enviar o erro
 +
        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;
 +
}
 +
</syntaxhighlight>
 +
 
 +
<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.
 +
 
 +
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 />
 +
 
 +
=== 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:
 +
 
 +
::<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 />
 +
 
 +
=== 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 />
 +
 
 +
=== Modificación mediante un procedemento almacenado ===
 +
 
 +
Tamén podemos realizar a modificación mediante un procedemento almacenado.
 +
 
 +
Por exemplo:
 +
 
 +
::<syntaxhighlight lang="sql" enclose="div">
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `MODIFICAR_ESTADO`(_id_estado int,_descripcion varchar(100))
+
CREATE PROCEDURE ACTUALIZAR_ESTADO_PROC(
 +
    IN p_id INT,
 +
    IN p_descripcion VARCHAR(100)
 +
)
 
BEGIN
 
BEGIN
  
−
UPDATE ESTADOS
+
    UPDATE estados
−
SET descripcion=_descripcion
+
    SET descripcion = p_descripcion
−
WHERE id_estado = _id_estado;
+
    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>
 +
 
 +
E desde PHP:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    PROCEDEMENTO_ACTUALIZAR_ESTADO
 +
);
 +
 
 +
$stmt->execute([
 +
    ':id_estado' => $id,
 +
    ':descripcion' => $descripcion
 +
]);
 +
 
 +
$fila = $stmt->fetch();
 +
 
 +
$stmt->closeCursor();
  
−
END
+
$numRexistros =
−
</syntaxhighlight>  
+
    (int) $fila['rexistros_modificados'];
 +
</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: bd.inc.php'''
+
=== mysqli como ampliación ===
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('PROCEDEMENTO_MODIFICAR_ESTADO',"BORRAR_ESTADO(%d,'%s')");
 
−
</syntaxhighlight>
 
  
 +
PDO será o mecanismo principal dos apuntes, pero podemos realizar a mesma operación empregando <code>mysqli</code>.
  
 +
<br />
  
 +
==== Conexión con mysqli ====
  
−
'''Arquivo: Xestionar_Estados_Proc.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="59-88,120-140" >
 
 
<?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 />
 +
 
 +
==== 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>
 +
 
 +
Para modificar:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    ACTUALIZAR_ESTADO_MYSQLI
 +
);
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
$stmt->bind_param(
−
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
     'si',
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
    $descripcion,
−
     <head>
+
     $id
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
);
−
        <title>Xestionar t&aacute;boa Estados BD</title>
 
−
    </head>
 
  
−
    <body>
+
$stmt->execute();
−
        <!-- 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!!!
+
$numRexistros = $stmt->affected_rows;
−
        if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion']=='BAIXA'){ 
+
</syntaxhighlight>
−
            $id_estado = $_POST['lstEstados'];
 
−
            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
 
−
       
 
−
        // OPERACION DE MODIFICAR
 
−
        if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion']=='MODIFICAR'){ 
 
−
            if (empty($_POST['lstEstados'])){
 
−
                $erro='É necesario seleccionar un estado da lista...';
 
−
            }
 
−
            else {
 
−
                $id_estado = $_POST['lstEstados'];
 
−
                if (!filter_var($id_estado,FILTER_VALIDATE_INT)){
 
−
                    $erro='<h4>O estado indicado non &eacute; v&aacute;lido...</h4>';
 
−
                }
 
−
                elseif (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_MODIFICAR_ESTADO,$conex->mysql_real_escape($id_estado),$novoEstado);
 
−
                    $proc = sprintf("CALL %s",$proc);
 
−
                    if ($conex->multi_query($proc)){
 
−
                        printf("<h4>Num. rexistros modificados:%d</h4>", $conex->affected_rows);
 
−
                        while($conex->next_result() && $conex->store_result());
 
−
                    }
 
−
                    else{
 
−
                        echo "<h5>Falló CALL: (" . $conex->errno . ") " . $conex->error . "</h5>";
 
−
                    }
 
  
−
                    }
+
Neste caso:
−
                }
 
−
        }  // Fin da operación de MODIFICAR
 
−
       
 
  
 +
:* <code>s</code> indica unha cadea.
 +
:* <code>i</code> indica un enteiro.
  
−
        // CARGAMOS AS DIFERENTES OPCIONS DE FORMULARIO EN BASE A OPERACION ESCOLLIDA DOS RADIOBUTTON: ALTA-BAIXA-MODIFICACION
+
Non necesitamos empregar:
−
        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;
 
−
                case 'MODIFICACION':
 
−
                    printf("<div>");
 
−
                    $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_ESTADOS);
 
−
                    printf("<select name='lstEstados' size='10'>");
 
−
                    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("Novo valor: <input name='txtEstado' type='text' size='50' maxlength='100' />");
 
−
                    printf("</div>");
 
−
                    printf("<div><input name='btnOperacion' type='submit' value='MODIFICAR' />");
 
−
                    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>
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$conex->real_escape_string(...)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
nin construír a consulta con <code>sprintf()</code>, porque os valores se envían mediante unha consulta preparada.
  
 +
<br />
  
 +
=== Exercicios propostos ===
  
 +
* De todos os exercicios fai dúas versións, unha empregando a orde SQL UPDATE e outra chamando a un procedemento almacenado de Mysql.
 +
 +
 +
<br />
 +
==== Modificación de Autores ====
 +
 +
* Ao carón de cada autor, amosa enlace para cargar o formulario de modificación de autores.
 +
: O campo id non debe aparecer visualmente no formulario.
 +
: Emprega o mesmo arquivo de campos_?????.php onde se gardan os datos no formulario.
 +
 +
 +
 +
 +
 +
 +
<br />
 +
 +
==== Modificación de Libros ====
 +
 +
* Ao carón de cada libro, amosa enlace para cargar o formulario de modificación de libros.
 +
: O campo id non debe aparecer visualmente no formulario.
 +
: Emprega o mesmo arquivo de campos_?????.php onde se gardan os datos no formulario.
 +
 +
 +
 +
 +
<br />
 +
==== Modificación de Exemplares ====
 +
 +
* Ao carón de cada exemplar, amosa enlace para cargar o formulario de modificación de exemplares.
 +
: O campo id non debe aparecer visualmente no formulario.
 +
: Emprega o mesmo arquivo de campos_?????.php onde se gardan os datos no formulario.
 +
 +
 +
 +
 +
 +
 +
 +
 +
<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]] -- (2023).
+
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2026).

Revisión actual del 09:43 6 oct 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.



  • 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

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'
    ) {

        header(
            'Location: consultar_estados.php?error=' .
            urlencode('O identificador non é válido.')
        );
        exit;
        /* Outra forma de enviar o erro
        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.


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.



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.


Exercicios propostos

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



Modificación de Autores

  • Ao carón de cada autor, amosa enlace para cargar o formulario de modificación de autores.
O campo id non debe aparecer visualmente no formulario.
Emprega o mesmo arquivo de campos_?????.php onde se gardan os datos no formulario.





Modificación de Libros

  • Ao carón de cada libro, amosa enlace para cargar o formulario de modificación de libros.
O campo id non debe aparecer visualmente no formulario.
Emprega o mesmo arquivo de campos_?????.php onde se gardan os datos no formulario.




Modificación de Exemplares

  • Ao carón de cada exemplar, amosa enlace para cargar o formulario de modificación de exemplares.
O campo id non debe aparecer visualmente no formulario.
Emprega o mesmo arquivo de campos_?????.php onde se gardan os datos no formulario.






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).