Diferencia entre revisiones de «PHP UD3 EXERCICIOS PROPOSTOS»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 31 ediciones intermedias del mismo usuario)
Línea 1: Línea 1:
−
==Introducción==
+
== Introdución ==
  
−
* <u>Notas:</u>
+
Nesta unidade propoñemos varios exercicios completos de mantemento de datos empregando PHP 8.5 e un servidor MySQL 8.0 ou 8.4.
−
:* A base de datos que imos empregar para facer os exercicios propostos a emprego para explicar outro módulo (Bases de datos) polo que non segue as [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Regras_de_nomeado_de_t.C3.A1boas_e_columnas regras de nomenclatura que indiqué anteriormente]. Podedes deixala así ou modificar o script para adaptalo a ditas regras.
 
−
:* Vou propoñer varios exercicios. O normal sería ter unha estrutura lóxica de cartafoles, onde gardaríamos as páxinas php que conforman o noso sitio web, e tamén unha cabeceira cun menú onde poderíamos navegar ata as diferentes páxinas (lembrade [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Reutilizaci%C3%B3n_de_c%C3%B3digo:_Include_/_Require o emprego de include]).
 
−
:* De cada exercicio podedes facer varias versións, facendo que as operacións contra a base de datos se fagan con ordes SQL ou con chamadas a procedementos creados previamente no xestor de Mysql.
 
−
:* Os exercicios terán código javascript, para confirmar o borrado das filas.  
 
  
 +
O obxectivo é practicar as operacións fundamentais dun CRUD:
  
 +
:* '''C'''reate → <code>INSERT</code>.
 +
:* '''R'''ead → <code>SELECT</code>.
 +
:* '''U'''pdate → <code>UPDATE</code>.
 +
:* '''D'''elete → <code>DELETE</code>.
  
 +
Nos exemplos completos empregaremos '''PDO (PHP Data Objects)''' como mecanismo principal de acceso á base de datos.
 +
 +
Ao final inclúese unha sección comparativa con <code>mysqli</code>. Nos exercicios e prácticas deste módulo empregarase '''PDO''' salvo indicación expresa en contrario, seguindo os [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql apuntes de operacións contra MySQL].
  
 
<br />
 
<br />
−
* Todos os exercicios propostos están baseados nunha base de datos de nome CIRCO probado sobre un xestor MYSQL versión 5.7 ou superior:
+
 
 +
* '''NOTA IMPORTANTE:''' cando empregamos datos procedentes dun formulario debemos:
 +
 
 +
:* validar os datos recibidos no servidor;
 +
:* empregar consultas preparadas cando unha orde SQL conteña datos variables;
 +
:* escapar os datos no momento de xerar a saída HTML;
 +
:* non mostrar ao usuario os detalles técnicos das excepcións;
 +
:* empregar <code>POST</code> para as operacións que modifican datos;
 +
:* comprobar os permisos ou autorización do usuario nunha aplicación real.
 +
 
 +
 
 +
 
 +
A protección fronte a ataques '''CSRF (Cross-Site Request Forgery)''' incorporarase cando se estuden [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Sesi%C3%B3ns#Ataques_CSRF_.28Cross-Site_Request_Forgery.29 as variables de sesións]. Empregar <code>POST</code> non evita por si só un ataque CSRF.
 +
 
 +
<br />
 +
 
 +
=== Consideracións sobre os exercicios ===
 +
 
 +
* Todos os exercicios están baseados na base de datos '''CIRCO''' empregada noutro módulo.
 +
 
 
[[Image:Mod_BD_Prog_Modelo_Circo.jpg|700px|center]]
 
[[Image:Mod_BD_Prog_Modelo_Circo.jpg|700px|center]]
  
−
:* Fichero .sql: [[File:CIRCO.zip]]
+
:* Ficheiro SQL: [[File:CIRCO.zip]]
 +
 
 +
* A base de datos ten algunhas características pouco habituais nunha aplicación real porque está deseñada tamén para practicar SQL.
 +
 
 +
Por exemplo, nalgunhas táboas as claves primarias son cadeas visibles para o usuario:
 +
 
 +
::<syntaxhighlight lang="text" enclose="div">
 +
PISTAS.nombre
 +
ANIMALES.nombre
 +
</syntaxhighlight>
 +
 
 +
Isto implica que, nunha modificación, o usuario pode cambiar o valor da clave primaria.
 +
 
 +
Por tanto teremos que conservar:
 +
 
 +
:* o valor orixinal da clave primaria, para localizar a fila;
 +
:* o novo valor introducido polo usuario, para actualizar a fila.
 +
 
 +
Nunha aplicación habitual é frecuente empregar como clave primaria un identificador numérico autonumérico que o usuario non modifica.
 +
 
 
<br />
 
<br />
  
 +
* Se o usuario modifica unha clave primaria e o novo valor xa existe, a base de datos producirá un erro de clave duplicada.
 +
 +
Podemos tratar esta situación comprobando previamente se o novo valor existe ou deixando que a restrición da base de datos detecte o conflito e capturando a excepción. A restrición da base de datos debe existir igualmente.
 +
 +
<br />
 +
 +
* O usuario empregado pola aplicación debe ter unicamente os permisos necesarios. Para estes exercicios serán normalmente <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code> e <code>DELETE</code>. O permiso <code>EXECUTE</code> só será necesario se tamén se practican procedementos almacenados.
 +
 +
* Os arquivos PHP estarán dentro da estrutura do sitio web, pero o arquivo coas credenciais e consultas estará fóra de <code>/var/www/html</code>.
 +
 +
Empregaremos:
 +
 +
::<syntaxhighlight lang="text" enclose="div">
 +
/var/www/confBD/bd.php
 +
</syntaxhighlight>
  
  
 +
* '''PASOS A SEGUIR E CONSIDERACIÓNS:'''
 +
:* Importar a base de datos <code>CIRCO</code> en MySQL 8 e comprobar a estrutura e as restricións das táboas.
 +
:* Crear un usuario específico con permisos mínimos (<code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code> e <code>DELETE</code>) sobre as táboas necesarias. Non empregar <code>root</code> na aplicación.
 +
:* Gardar as credenciais e as constantes SQL en <code>/var/www/confBD/bd.php</code>, fóra do directorio público do sitio web.
 +
:* Durante o desenvolvemento consultar os erros no rexistro do servidor e empregar un depurador cando sexa posible. A opción <code>display_errors</code> non debe estar activa en produción.
 +
:* Configurar os permisos de forma que o usuario do servidor web poida ler os scripts e o arquivo de configuración, sen conceder escritura onde non sexa necesaria. Evitar que outros usuarios locais poidan ler as credenciais.
 +
:* Manter separados os formularios HTML e os scripts de procesamento de <code>INSERT</code>, <code>UPDATE</code> e <code>DELETE</code>.
 +
:* As consultas preparadas non deben reconstruírse concatenando datos do formulario; os valores variables enlázanse con parámetros.
  
 
<br />
 
<br />
−
* '''PASOS A SEGUIR E CONSIDERACIÓNS:'''
 
−
:* Crear a base de datos anterior (CIRCO) executando o script no xestor MYSQL
 
−
:* 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 de nome '''bd_pistas.inc.php''' 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].
 
−
:* Na maior parte das bases de datos, as claves primarias son id´s autonuméricos que non se amosan ao usuario e quedan en campos ocultos. Á hora de actualizar os datos dunha táboa o id se emprega para buscar o dato a actualizar e nunca o pode cambiar o usuario no formulario. Nesta base de datos, as claves primarias son datos que se amosan ao usuario e os vai poder cambiar. Polo tanto nos formularios de modificación imos ter que enviar a clave primaria orixinal (sen cambiar) e a clave primaria modificada xa que imos ter que buscar a fila pola clave primaria sen modificar e cambiar o seu valor.
 
−
:* Relacionado có anterior, podería darse o caso de que ao modificar un dato, a clave primaria estivera repetida. Nas solucións non contemplo este caso. A solución é doada: Ou facer unha consulta previa para verificar que a nova clave primaria non existe ou capturar a excepción 'duplicate key' e amosar unha mensaxe ao usuario.
 
−
:* Lembrar que en todos os exercicios aparecen as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas. E tamén se visualizan os erros que se producen cando hai unha excepción (sección catch). Isto tería que modificarse xa que ao usuario non se lle deben amosar información sobre os erros que se poidan dar.
 
−
:* Lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755.
 
  
 +
== Para todos os exercicios ==
 +
 +
=== Arquivo /var/www/confBD/bd.php ===
 +
 +
O arquivo terá inicialmente:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
 +
// Arquivo de configuración fóra do directorio público.
 +
define('SERVIDOR', 'localhost');
 +
define('USUARIO', 'user_php');
 +
define('PASSWORD', 'user_php');
 +
define('BD', 'CIRCO');
 +
</syntaxhighlight>
  
 +
Neste mesmo arquivo iremos engadindo as constantes coas consultas necesarias para os diferentes exercicios.
  
 
<br />
 
<br />
−
==Para todos os exercicios==
 
  
−
'''Arquivo bd_pistas.inc.php''': Dito arquivo se atopa nun cartafol confBD fora da estrutura do sitio web.
+
=== Esquema común para acceder á base de datos ===
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
 
 +
Nas páxinas PHP que accedan á base de datos empregaremos o seguinte esquema:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
  
−
define ('SERVIDOR','localhost');
+
try {
−
define ('USUARIO','user_php');
+
 
−
define ('PASSWORD','user_php');
+
    require '/var/www/confBD/bd.php';
−
define ('BD','CIRCO');
+
 
 +
    $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,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
    // 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>
 
</syntaxhighlight>
  
 +
<br />
  
 +
=== Consultas con e sen parámetros ===
 +
 +
Se unha consulta non ten valores variables, podemos utilizar directamente <code>query()</code>:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->query(CONSULTAR_PISTAS);
 +
$pistas = $stmt->fetchAll();
 +
</syntaxhighlight>
 +
 +
Se a consulta contén valores variables, empregaremos <code>prepare()</code> e <code>execute()</code>:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    CONSULTAR_PISTA_POR_NOME
 +
);
 +
 +
$stmt->execute([
 +
    ':nombre' => $nombre
 +
]);
 +
 +
$pista = $stmt->fetch();
 +
</syntaxhighlight>
 +
 +
Non construiremos as consultas inserindo directamente os datos mediante <code>sprintf()</code>, concatenación ou interpolación.
  
 
<br />
 
<br />
−
==Exercicio 1==
 
  
−
* Partindo da táboa PISTAS, crea unha páxina web que permita dar de alta, baixa, modificar e listar os datos de dita táboa.
+
=== Saída HTML ===
−
: No listado amosa unha táboa cós datos e na parte dereita un enlace para modificar unha pista e un botón para borrala. Se sabes, fai que o botón teña a apariencia dun enlace empregando css. Se sabes javascript, fai que o botón pida conformación de borrado.
 
−
: <u>Crea un cartafol  pistas</u> onde van estar todas as páxinas relacioanadas con pistas.
 
−
: Como as operacións de alta e modificar van amosar os mesmos campos, fai que os campos estean gardados nun arquivo separado de nome 'campos_pistas.php'. Deberán amosar o dato en caso de que carguemos o formulario ao editar unha pista ou amosar os campos en branco no caso de que viñamos de dar de alta unha nova pista. Isto o podes facer empregando variables que veñan cos datos en caso de editar e se ditas variables non están definidas é que vimos do formulario de alta.
 
−
: Fai que o action do formulario de alta pista, apunte a unha páxina de nome procesar_alta_pista.php que é onde terás o código para dar de alta unha pista. Ao dala de alta terás que redirexionar á páxinas de listado de pistas.
 
−
: Fai que o action do formulario de modificar pista, apunte a unha páxina de nome procesar_modificar_pista.php que é onde terás o código para modificar unha pista. Ao modificala terás que redirexionar á páxinas de listado de pistas.
 
−
: Polo tanto as páxinas a facer son: listar_pistas.php, alta_pista.php, procesar_alta_pista.php, editar_pista.php, procesar_editar_pista.php, procesar_borrar_pista.php
 
  
 +
Os datos gardaranse normalmente na base de datos na súa forma orixinal.
  
 +
Aplicaremos <code>htmlspecialchars()</code> cando os introduzamos nun documento HTML:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?= htmlspecialchars(
 +
    $pista['nombre'],
 +
    ENT_QUOTES | ENT_SUBSTITUTE,
 +
    'UTF-8'
 +
) ?>
 +
</syntaxhighlight>
 +
 +
Para un valor que pode ser <code>NULL</code> podemos facer:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?= htmlspecialchars(
 +
    (string) ($animal['nombre_pista'] ?? ''),
 +
    ENT_QUOTES | ENT_SUBSTITUTE,
 +
    'UTF-8'
 +
) ?>
 +
</syntaxhighlight>
 +
 +
Deste xeito <code>NULL</code> represéntase como unha cadea baleira na saída HTML.
 +
 +
<br />
 +
 +
== Exercicio 1: mantemento da táboa PISTAS ==
 +
 +
Partindo da táboa <code>PISTAS</code>, crea unha aplicación que permita listar, dar de alta, modificar e borrar pistas.
 +
 +
Crearemos o cartafol:
 +
 +
::<syntaxhighlight lang="text" enclose="div">
 +
pistas/
 +
</syntaxhighlight>
 +
 +
coas seguintes páxinas:
 +
 +
::<syntaxhighlight lang="text" enclose="div">
 +
consultar_pistas.php
 +
alta_pista.php
 +
campos_pista.php
 +
procesar_alta_pista.php
 +
editar_pista.php
 +
procesar_editar_pista.php
 +
procesar_borrar_pista.php
 +
</syntaxhighlight>
  
 
<gallery caption="Xestión da táboa PISTAS" widths="350" heights="300px" perrow="2">
 
<gallery caption="Xestión da táboa PISTAS" widths="350" heights="300px" perrow="2">
Línea 67: Línea 234:
 
Image:UD3_op_bd_ex_1_B.jpg| Formulario de alta de pista.
 
Image:UD3_op_bd_ex_1_B.jpg| Formulario de alta de pista.
 
Image:UD3_op_bd_ex_1_C.jpg| Formulario de modificación de pista.
 
Image:UD3_op_bd_ex_1_C.jpg| Formulario de modificación de pista.
−
Image:UD3_op_bd_ex_1_D.jpg| Confirmación borrado de pista.
+
Image:UD3_op_bd_ex_1_D.jpg| Confirmación de borrado de pista.
 
</gallery>
 
</gallery>
  
 +
<br />
 +
 +
=== Consultas necesarias ===
 +
 +
Engadimos ao arquivo <code>/var/www/confBD/bd.php</code>:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'CONSULTAR_PISTAS',
 +
    'SELECT nombre, aforo
 +
    FROM PISTAS
 +
    ORDER BY nombre'
 +
);
 +
 +
define(
 +
    'CONSULTAR_PISTA_POR_NOME',
 +
    'SELECT nombre, aforo
 +
    FROM PISTAS
 +
    WHERE nombre = :nombre'
 +
);
 +
 +
define(
 +
    'INSERIR_PISTA',
 +
    'INSERT INTO PISTAS (nombre, aforo)
 +
    VALUES (:nombre, :aforo)'
 +
);
 +
 +
define(
 +
    'ACTUALIZAR_PISTA',
 +
    'UPDATE PISTAS
 +
    SET nombre = :nombre_novo,
 +
        aforo = :aforo
 +
    WHERE nombre = :nombre_orixinal'
 +
);
  
 +
define(
 +
    'BORRAR_PISTA',
 +
    'DELETE FROM PISTAS
 +
    WHERE nombre = :nombre'
 +
);
 +
</syntaxhighlight>
  
 
<br />
 
<br />
−
===Solución Exercicio 1===
+
 
−
'''Arquivo bd_pistas.inc.php''': Engadir o seguinte contido ao existente.
+
=== Arquivo pistas/consultar_pistas.php ===
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
  
−
define('CONSULTAR_PISTAS', 'SELECT nombre,aforo FROM PISTAS ORDER BY nombre');
+
try {
−
define('ACTUALIZAR_PISTAS', "UPDATE PISTAS SET nombre='%s',aforo = %d WHERE nombre = '%s'");
+
 
−
define('INSERTAR_PISTAS', "INSERT INTO PISTAS (nombre, aforo) VALUES ('%s', %d)");
+
    require '/var/www/confBD/bd.php';
−
define('BORRAR_PISTAS', "DELETE FROM PISTAS WHERE nombre='%s'");  
+
 
−
define('CONSULTAR_PISTAS_POR_NOME', "SELECT nombre,aforo FROM PISTAS WHERE nombre = '%s'");
+
    $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,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
    $stmt = $conex->query(CONSULTAR_PISTAS);
 +
    $pistas = $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 pistas</title>
 +
</head>
 +
<body>
 +
 
 +
    <h1>Xestión de pistas</h1>
 +
 
 +
    <form action="alta_pista.php" method="get">
 +
        <button type="submit">Nova pista</button>
 +
    </form>
 +
 
 +
    <table>
 +
        <thead>
 +
            <tr>
 +
                <th>Nome</th>
 +
                <th>Aforo</th>
 +
                <th>Accións</th>
 +
            </tr>
 +
        </thead>
 +
        <tbody>
 +
 
 +
        <?php foreach ($pistas as $pista): ?>
 +
 
 +
            <tr>
 +
                <td><?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars((string) $pista['aforo'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td>
 +
                    <a href="editar_pista.php?nombre=<?= urlencode($pista['nombre']) ?>">Editar</a>
 +
 
 +
                    <form
 +
                        action="procesar_borrar_pista.php"
 +
                        method="post"
 +
                        style="display:inline"
 +
                        onsubmit="return confirm('Estás seguro de que queres borrar esta pista?');"
 +
                    >
 +
                        <input
 +
                            type="hidden"
 +
                            name="nombre"
 +
                            value="<?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
                        >
 +
                        <button type="submit" name="btnBorrarPista" 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>
 +
 
 +
<br />
 +
 
 +
=== Arquivo pistas/campos_pista.php ===
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<label for="nombre">Nome:</label>
 +
<input
 +
    type="text"
 +
    id="nombre"
 +
    name="nombre"
 +
    maxlength="50"
 +
    required
 +
    value="<?= htmlspecialchars($nombre ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
>
 +
<br>
 +
 +
<label for="aforo">Aforo:</label>
 +
<input
 +
    type="number"
 +
    id="aforo"
 +
    name="aforo"
 +
    min="20"
 +
    max="500"
 +
    required
 +
    value="<?= htmlspecialchars(isset($aforo) ? (string) $aforo : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
A validación HTML mellora a experiencia do usuario, pero os mesmos criterios deben comprobarse novamente no servidor.
  
 +
<br />
  
 +
=== Arquivo pistas/alta_pista.php ===
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<!DOCTYPE html>
 +
<html lang="gl">
 +
<head>
 +
    <meta charset="UTF-8">
 +
    <title>Alta de pista</title>
 +
</head>
 +
<body>
 +
 +
    <h1>Alta de pista</h1>
  
−
<br/>
+
    <form method="post" action="procesar_alta_pista.php">
 +
        <?php require 'campos_pista.php'; ?>
 +
        <div>
 +
            <button type="submit" name="btnAltaPista" value="ALTA">Dar de alta</button>
 +
        </div>
 +
    </form>
  
−
'''Arquivo '''pistas/listar_pistas.php''': Fixarse que está dentro dun cartafol creado previamente de nome pistas.
+
</body>
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
</html>
 +
</syntaxhighlight>
 +
 
 +
<br />
 +
 
 +
=== Arquivo pistas/procesar_alta_pista.php ===
 +
 
 +
::<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_pistas.inc.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,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnAltaPista']) || $_POST['btnAltaPista'] !== 'ALTA') {
 +
        throw new RuntimeException('Operación non válida.');
 +
    }
 +
 
 +
    $nombre = trim($_POST['nombre'] ?? '');
 +
 
 +
    $aforo = filter_input(
 +
        INPUT_POST,
 +
        'aforo',
 +
        FILTER_VALIDATE_INT,
 +
        [
 +
            'options' => [
 +
                'min_range' => 20,
 +
                'max_range' => 500
 +
            ]
 +
        ]
 +
    );
 +
 
 +
    if ($nombre === '' || mb_strlen($nombre, 'UTF-8') > 50) {
 +
        header('Location: consultar_pistas.php?error=' . urlencode('O nome da pista non é válido.'));
 +
        exit;
 +
    }
 +
 
 +
    if ($aforo === false || $aforo === null) {
 +
        header('Location: consultar_pistas.php?error=' . urlencode('O aforo non é válido.'));
 +
        exit;
 +
    }
 +
 
 +
    $stmt = $conex->prepare(INSERIR_PISTA);
 +
    $stmt->bindValue(':nombre', $nombre, PDO::PARAM_STR);
 +
    $stmt->bindValue(':aforo', $aforo, PDO::PARAM_INT);
 +
    $stmt->execute();
 +
 
 +
    header('Location: consultar_pistas.php?msg=' . urlencode('Pista dada de alta correctamente.'));
 +
    exit;
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    $codigoMySQL = $e->errorInfo[1] ?? null;
 +
 
 +
    $mensaxe = $codigoMySQL === 1062
 +
        ? 'Xa existe unha pista con ese nome.'
 +
        : 'Erro relacionado coa base de datos.';
 +
 
 +
    header('Location: consultar_pistas.php?error=' . urlencode($mensaxe));
 +
    exit;
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    header('Location: consultar_pistas.php?error=' . urlencode('Produciuse un erro na aplicación.'));
 +
    exit;
 +
}
 +
</syntaxhighlight>
 +
 
 +
<br />
 +
 
 +
=== Arquivo pistas/editar_pista.php ===
 +
 
 +
Esta páxina recibe o nome da pista mediante <code>GET</code>. É correcto porque a petición unicamente consulta os datos e amosa o formulario.
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
  
 
try {
 
try {
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
 +
    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,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 +
    $nombreRecibido = trim($_GET['nombre'] ?? '');
 +
 +
    if ($nombreRecibido === '') {
 +
        header('Location: consultar_pistas.php?error=' . urlencode('Non se indicou unha pista válida.'));
 +
        exit;
 +
    }
 +
 +
    $stmt = $conex->prepare(CONSULTAR_PISTA_POR_NOME);
 +
    $stmt->execute([':nombre' => $nombreRecibido]);
 +
    $pista = $stmt->fetch();
 +
 +
    if ($pista === false) {
 +
        header('Location: consultar_pistas.php?error=' . urlencode('Non se atopou a pista indicada.'));
 +
        exit;
 +
    }
 +
 +
    $nombre = $pista['nombre'];
 +
    $aforo = $pista['aforo'];
 +
 +
} 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>Modificar pista</title>
−
        <title>Xestión de pistas</title>
+
</head>
−
    </head>
+
<body>
−
    <body>
+
 
−
        <form action="alta_pista.php" method="post">
+
    <h1>Modificar pista</h1>
−
            <button type="submit">Nova Pista</button>
 
−
        </form>
 
−
        <table border="1">
 
−
            <tr>
 
−
            <th>Nombre</th>
 
−
            <th>Aforo</th>
 
−
            <th>Accións</th>
 
−
            </tr>
 
−
        <?php
 
−
            $resultado = $conex->query(CONSULTAR_PISTAS);
 
  
−
            while($fila = $resultado->fetch_assoc()) {
+
    <form method="post" action="procesar_editar_pista.php">
−
                $nombre = htmlentities($fila['nombre']);
+
        <?php require 'campos_pista.php'; ?>
−
                $aforo = htmlentities($fila['aforo']);
 
−
                echo "<tr>";
 
−
                echo "<td>$nombre</td>";
 
−
                echo "<td>$aforo</td>";
 
−
                printf("<td><a href='editar_pista.php?nombre=%s'>Editar</a>",$nombre);
 
−
                printf("<form action='procesar_borrar_pista.php' method='post' onsubmit='return confirm(\'Estás seguro de que queres borrar esta pista?\');'>");
 
−
                printf("<input type='hidden' name='nombre' value='%s' />",htmlentities($fila['nombre']));
 
−
                echo '<input onclick="return confirm(\'Estás seguro de que queres borrar esta pista?\');" type="submit" value="Borrar" name="btnBorrarPista" style="background:none;border:none;padding:0;maring:0;color: blue; text-decoration:underline; cursor:pointer; font-size:inherit" />';
 
−
                echo '</form>';
 
−
                echo "</tr>";
 
−
            }
 
  
−
             $resultado->free();
+
        <input
−
        ?>  
+
            type="hidden"
−
         </table>
+
            name="nome_orixinal"
 +
             value="<?= htmlspecialchars($nombre, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
         >
  
 +
        <button type="submit" name="btnModificarPista" value="MODIFICAR">Modificar</button>
 +
    </form>
  
−
       
+
</body>
−
    </body>  
 
 
</html>
 
</html>
 +
</syntaxhighlight>
 +
 +
Fixarse en que gardamos o nome orixinal nun campo oculto porque o usuario pode modificar <code>PISTAS.nombre</code>, que neste modelo é a clave primaria. Un campo <code>hidden</code> pode ser manipulado, polo que o servidor segue tratando o valor como un dato non confiable.
 +
 +
<br />
 +
 +
=== Arquivo pistas/procesar_editar_pista.php ===
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
−
}  // Pecha o try da parte superior da páxina
+
 
−
catch (mysqli_sql_exception $e) {
+
try {
−
     die("Erro mysql: " . $e->getMessage());
+
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnModificarPista']) || $_POST['btnModificarPista'] !== 'MODIFICAR') {
 +
        throw new RuntimeException('Operación non válida.');
 +
    }
 +
 
 +
    $nombreNovo = trim($_POST['nombre'] ?? '');
 +
    $nombreOrixinal = trim($_POST['nome_orixinal'] ?? '');
 +
 
 +
    $aforo = filter_input(
 +
        INPUT_POST,
 +
        'aforo',
 +
        FILTER_VALIDATE_INT,
 +
        [
 +
            'options' => [
 +
                'min_range' => 20,
 +
                'max_range' => 500
 +
            ]
 +
        ]
 +
    );
 +
 
 +
    if ($nombreNovo === '' || mb_strlen($nombreNovo, 'UTF-8') > 50 || $nombreOrixinal === '') {
 +
        header('Location: consultar_pistas.php?error=' . urlencode('Os datos da pista non son válidos.'));
 +
        exit;
 +
    }
 +
 
 +
    if ($aforo === false || $aforo === null) {
 +
        header('Location: consultar_pistas.php?error=' . urlencode('O aforo non é válido.'));
 +
        exit;
 +
    }
 +
 
 +
    $stmt = $conex->prepare(ACTUALIZAR_PISTA);
 +
    $stmt->bindValue(':nombre_novo', $nombreNovo, PDO::PARAM_STR);
 +
    $stmt->bindValue(':aforo', $aforo, PDO::PARAM_INT);
 +
    $stmt->bindValue(':nombre_orixinal', $nombreOrixinal, PDO::PARAM_STR);
 +
    $stmt->execute();
 +
 
 +
    $mensaxe = $stmt->rowCount() === 1
 +
        ? 'Pista actualizada correctamente.'
 +
        : 'Non se modificou ningunha fila (pode non existir ou non haber cambios).';
 +
 
 +
    header('Location: consultar_pistas.php?msg=' . urlencode($mensaxe));
 +
    exit;
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
     error_log($e->getMessage());
 +
    $codigoMySQL = $e->errorInfo[1] ?? null;
 +
 
 +
    $mensaxe = $codigoMySQL === 1062
 +
        ? 'Xa existe unha pista co novo nome indicado.'
 +
        : 'Erro relacionado coa base de datos.';
 +
 
 +
    header('Location: consultar_pistas.php?error=' . urlencode($mensaxe));
 +
    exit;
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    header('Location: consultar_pistas.php?error=' . urlencode('Produciuse un erro na aplicación.'));
 +
    exit;
 
}
 
}
−
catch (Expcetion $e) {
+
</syntaxhighlight>
−
     die("Erro xeral: " . $e->getMessage());
+
 
 +
<br />
 +
 
 +
=== Arquivo pistas/procesar_borrar_pista.php ===
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
 
 +
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnBorrarPista']) || $_POST['btnBorrarPista'] !== 'BORRAR') {
 +
        throw new RuntimeException('Operación non válida.');
 +
    }
 +
 
 +
    $nombre = trim($_POST['nombre'] ?? '');
 +
 
 +
    if ($nombre === '') {
 +
        header('Location: consultar_pistas.php?error=' . urlencode('Non se indicou unha pista válida.'));
 +
        exit;
 +
    }
 +
 
 +
    $stmt = $conex->prepare(BORRAR_PISTA);
 +
    $stmt->execute([':nombre' => $nombre]);
 +
 
 +
    $mensaxe = $stmt->rowCount() === 1
 +
        ? 'Pista borrada correctamente.'
 +
        : 'Non se atopou a pista que se quería borrar.';
 +
 
 +
    header('Location: consultar_pistas.php?msg=' . urlencode($mensaxe));
 +
    exit;
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
     error_log($e->getMessage());
 +
    $codigoMySQL = $e->errorInfo[1] ?? null;
 +
 
 +
    $mensaxe = $codigoMySQL === 1451
 +
        ? 'A pista está relacionada con outros datos e non se pode borrar.'
 +
        : 'Erro relacionado coa base de datos.';
 +
 
 +
    header('Location: consultar_pistas.php?error=' . urlencode($mensaxe));
 +
    exit;
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    header('Location: consultar_pistas.php?error=' . urlencode('Produciuse un erro na aplicación.'));
 +
    exit;
 
}
 
}
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
 +
== Exercicio 2: mantemento da táboa ANIMALES ==
 +
 +
A táboa <code>ANIMALES</code> incorpora varias características novas:
 +
 +
:* relacións con <code>PISTAS</code> e <code>ATRACCIONES</code>;
 +
:* campos que poden conter <code>NULL</code>;
 +
:* campos numéricos opcionais;
 +
:* listas <code>&lt;select&gt;</code> cuxas opcións se obteñen da base de datos;
 +
:* unha clave primaria textual que pode ser modificada polo usuario.
 +
 +
Os campos empregados no exercicio son:
 +
 +
::<syntaxhighlight lang="text" enclose="div">
 +
nombre
 +
tipo
 +
anhos
 +
peso
 +
estatura
 +
nombre_atraccion
 +
nombre_pista
 +
</syntaxhighlight>
 +
 +
Os campos opcionais son:
 +
 +
::<syntaxhighlight lang="text" enclose="div">
 +
anhos
 +
peso
 +
estatura
 +
nombre_atraccion
 +
nombre_pista
 +
</syntaxhighlight>
 +
 +
Crearemos o cartafol:
 +
 +
::<syntaxhighlight lang="text" enclose="div">
 +
animais/
 +
</syntaxhighlight>
 +
 +
coas seguintes páxinas:
  
 +
::<syntaxhighlight lang="text" enclose="div">
 +
consultar_animais.php
 +
alta_animal.php
 +
campos_animal.php
 +
procesar_alta_animal.php
 +
editar_animal.php
 +
procesar_editar_animal.php
 +
procesar_borrar_animal.php
 +
</syntaxhighlight>
  
 +
<gallery caption="Xestión da táboa ANIMALES" widths="350" heights="300px" perrow="2">
 +
Image:UD3_op_bd_ex_2_A.jpg| Listado de animais.
 +
Image:UD3_op_bd_ex_2_B.jpg| Formulario de alta de animal.
 +
Image:UD3_op_bd_ex_2_C.jpg| Formulario de modificación de animal.
 +
Image:UD3_op_bd_ex_2_D.jpg| Confirmación de borrado de animal.
 +
</gallery>
  
 
<br />
 
<br />
−
'''Arquivo pistas/campos_pista.php''':  
+
 
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
=== Valores NULL con PDO ===
−
Nome: <input type="text" maxlength="50" name="nombre" required value="<?php echo isset($nombre) ? htmlentities($nombre) : ''; ?>"><br>
+
 
−
Aforo: <input type="number" min="20" max="500" name="aforo" required value="<?php echo isset($aforo) ? htmlentities($aforo) : ''; ?>">
+
Con PDO en PHP representaremos a ausencia de valor mediante <code>null</code> e enlazaremos explicitamente o parámetro como <code>PDO::PARAM_NULL</code> cando sexa necesario:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt->bindValue(
 +
    ':anhos',
 +
    null,
 +
    PDO::PARAM_NULL
 +
);
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Deste xeito a base de datos recibe un valor SQL <code>NULL</code>, non a cadea de texto <code>'null'</code>.
 +
 +
<br />
 +
 +
=== Consultas necesarias ===
 +
 +
Engadimos ao arquivo <code>/var/www/confBD/bd.php</code>:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'CONSULTAR_ATRACCIONS',
 +
    'SELECT nombre, fecha_inicio, ganancias
 +
    FROM ATRACCIONES
 +
    ORDER BY nombre'
 +
);
 +
 +
define(
 +
    'CONSULTAR_ANIMAIS',
 +
    'SELECT
 +
        ANIMALES.nombre,
 +
        ANIMALES.tipo,
 +
        ANIMALES.anhos,
 +
        ANIMALES.peso,
 +
        ANIMALES.estatura,
 +
        ANIMALES.nombre_atraccion,
 +
        ATRACCIONES.fecha_inicio,
 +
        ANIMALES.nombre_pista,
 +
        PISTAS.aforo
 +
    FROM ANIMALES
 +
    LEFT JOIN ATRACCIONES
 +
        ON ANIMALES.nombre_atraccion = ATRACCIONES.nombre
 +
    LEFT JOIN PISTAS
 +
        ON ANIMALES.nombre_pista = PISTAS.nombre
 +
    ORDER BY ANIMALES.nombre'
 +
);
  
 +
define(
 +
    'CONSULTAR_ANIMAL_POR_NOME',
 +
    'SELECT
 +
        nombre,
 +
        tipo,
 +
        anhos,
 +
        peso,
 +
        estatura,
 +
        nombre_atraccion,
 +
        nombre_pista
 +
    FROM ANIMALES
 +
    WHERE nombre = :nombre'
 +
);
  
 +
define(
 +
    'INSERIR_ANIMAL',
 +
    'INSERT INTO ANIMALES (
 +
        nombre,
 +
        tipo,
 +
        anhos,
 +
        peso,
 +
        estatura,
 +
        nombre_atraccion,
 +
        nombre_pista
 +
    ) VALUES (
 +
        :nombre,
 +
        :tipo,
 +
        :anhos,
 +
        :peso,
 +
        :estatura,
 +
        :nombre_atraccion,
 +
        :nombre_pista
 +
    )'
 +
);
 +
 +
define(
 +
    'ACTUALIZAR_ANIMAL',
 +
    'UPDATE ANIMALES
 +
    SET nombre = :nombre_novo,
 +
        tipo = :tipo,
 +
        anhos = :anhos,
 +
        peso = :peso,
 +
        estatura = :estatura,
 +
        nombre_atraccion = :nombre_atraccion,
 +
        nombre_pista = :nombre_pista
 +
    WHERE nombre = :nombre_orixinal'
 +
);
 +
 +
define(
 +
    'BORRAR_ANIMAL',
 +
    'DELETE FROM ANIMALES
 +
    WHERE nombre = :nombre'
 +
);
 +
</syntaxhighlight>
 +
 +
Reutilizaremos tamén <code>CONSULTAR_PISTAS</code>, definida no exercicio anterior.
  
 
<br />
 
<br />
−
'''Arquivo pistas/alta_pista.php'''
+
 
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
=== Arquivo animais/consultar_animais.php ===
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
 
 +
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
    $stmt = $conex->query(CONSULTAR_ANIMAIS);
 +
    $animais = $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>
 
<!DOCTYPE html>
−
<html>
+
<html lang="gl">
 
<head>
 
<head>
−
  <title>Alta Pista</title>
+
    <meta charset="UTF-8">
 +
    <title>Xestión de animais</title>
 
</head>
 
</head>
 
<body>
 
<body>
−
  <h3>Alta pista</h3>
+
 
−
  <form method="POST" action="procesar_alta_pista.php">
+
    <h1>Xestión de animais</h1>
−
     <?php include 'campos_pista.php'; ?>
+
 
−
    <div>
+
    <form action="alta_animal.php" method="get">
−
        <input type="submit" value="Dar de Alta" name="btnAltaPista">
+
        <button type="submit">Novo animal</button>
−
     </div>
+
     </form>
−
  </form>
+
 
 +
    <table>
 +
        <thead>
 +
            <tr>
 +
                <th>Nome</th>
 +
                <th>Tipo</th>
 +
                <th>Anos</th>
 +
                <th>Peso</th>
 +
                <th>Estatura</th>
 +
                <th>Atracción</th>
 +
                <th>Data inicio</th>
 +
                <th>Pista</th>
 +
                <th>Aforo</th>
 +
                <th>Accións</th>
 +
            </tr>
 +
        </thead>
 +
        <tbody>
 +
 
 +
        <?php foreach ($animais as $animal): ?>
 +
 
 +
            <?php
 +
                $fechaInicio = '';
 +
 
 +
                if ($animal['fecha_inicio'] !== null) {
 +
                    $fechaInicio = (new DateTime($animal['fecha_inicio']))->format('d-m-Y');
 +
                }
 +
            ?>
 +
 
 +
            <tr>
 +
                <td><?= htmlspecialchars($animal['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars($animal['tipo'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars((string) ($animal['anhos'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars((string) ($animal['peso'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars((string) ($animal['estatura'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars((string) ($animal['nombre_atraccion'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars($fechaInicio, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars((string) ($animal['nombre_pista'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td><?= htmlspecialchars((string) ($animal['aforo'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
 +
                <td>
 +
                    <a href="editar_animal.php?nombre=<?= urlencode($animal['nombre']) ?>">Editar</a>
 +
 
 +
                    <form
 +
                        action="procesar_borrar_animal.php"
 +
                        method="post"
 +
                        style="display:inline"
 +
                        onsubmit="return confirm('Estás seguro de que queres borrar este animal?');"
 +
                    >
 +
                        <input
 +
                            type="hidden"
 +
                            name="nombre"
 +
                            value="<?= htmlspecialchars($animal['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
                        >
 +
                        <button type="submit" name="btnBorrarAnimal" 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>
 
</body>
 
</html>
 
</html>
 +
</syntaxhighlight>
 +
 +
Como empregamos un <code>LEFT JOIN</code>, <code>fecha_inicio</code> pode chegar como <code>NULL</code>. Comprobámolo antes de crear o obxecto <code>DateTime</code>.
 +
 +
<br />
 +
 +
=== Carga das pistas e atraccións dos formularios ===
 +
 +
Os formularios de alta e modificación necesitan todas as pistas e atraccións dispoñibles.
 +
 +
 +
 +
<br />
 +
 +
=== Arquivo animais/campos_animal.php ===
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<label for="nombre">Nome:</label>
 +
<input
 +
    type="text"
 +
    id="nombre"
 +
    name="nombre"
 +
    required
 +
    value="<?= htmlspecialchars($nombre ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
>
 +
<br>
 +
 +
<label for="tipo">Tipo:</label>
 +
<select id="tipo" name="tipo" required>
 +
 +
    <?php
 +
        $tipos = [
 +
            'León',
 +
            'Cocodrilo',
 +
            'Jirafa',
 +
            'Mono'
 +
        ];
 +
    ?>
 +
 +
    <?php foreach ($tipos as $tipoOption): ?>
 +
        <option
 +
            value="<?= htmlspecialchars($tipoOption, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
            <?= ($tipo ?? '') === $tipoOption ? 'selected' : '' ?>
 +
        >
 +
            <?= htmlspecialchars($tipoOption, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
 +
        </option>
 +
    <?php endforeach; ?>
 +
 +
</select>
 +
<br>
 +
 +
<label for="anhos">Anos:</label>
 +
<input
 +
    type="number"
 +
    id="anhos"
 +
    name="anhos"
 +
    min="1"
 +
    max="50"
 +
    value="<?= htmlspecialchars(isset($anhos) ? (string) $anhos : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
>
 +
<br>
 +
 +
<label for="peso">Peso:</label>
 +
<input
 +
    type="number"
 +
    id="peso"
 +
    name="peso"
 +
    min="0.1"
 +
    max="500"
 +
    step="any"
 +
    value="<?= htmlspecialchars(isset($peso) ? (string) $peso : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
>
 +
<br>
 +
 +
<label for="estatura">Estatura:</label>
 +
<input
 +
    type="number"
 +
    id="estatura"
 +
    name="estatura"
 +
    min="0.1"
 +
    max="20"
 +
    step="any"
 +
    value="<?= htmlspecialchars(isset($estatura) ? (string) $estatura : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
>
 +
<br>
 +
 +
<label for="nombre_atraccion">Nome atracción:</label>
 +
<select id="nombre_atraccion" name="nombre_atraccion">
 +
    <option value="">Sen asignar</option>
  
 +
    <?php foreach ($atraccions as $atraccion): ?>
 +
        <option
 +
            value="<?= htmlspecialchars($atraccion['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
            <?= ($nombre_atraccion ?? null) === $atraccion['nombre'] ? 'selected' : '' ?>
 +
        >
 +
            <?= htmlspecialchars($atraccion['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
 +
        </option>
 +
    <?php endforeach; ?>
 +
 +
</select>
 +
<br>
 +
 +
<label for="nombre_pista">Nome pista:</label>
 +
<select id="nombre_pista" name="nombre_pista">
 +
    <option value="">Sen asignar</option>
 +
 +
    <?php foreach ($pistas as $pista): ?>
 +
        <option
 +
            value="<?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
            <?= ($nombre_pista ?? null) === $pista['nombre'] ? 'selected' : '' ?>
 +
        >
 +
            <?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
 +
        </option>
 +
    <?php endforeach; ?>
 +
 +
</select>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
A comparación para decidir se unha opción está seleccionada faise cos valores orixinais. O escapado con <code>htmlspecialchars()</code> aplícase unicamente cando o valor se introduce no HTML. Por tanto, os acentos non producen problemas nas comparacións.
  
 +
<br />
  
 +
=== Arquivo animais/alta_animal.php ===
  
−
<br />
+
::<syntaxhighlight lang="php" enclose="div">
−
'''Arquivo pistas/procesar_alta_pista.php'''
 
−
::<syntaxhighlight lang="php" line 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_pistas.inc.php');
+
try {
−
if (isset($_POST['btnAltaPista'])) {
+
 
−
  try {
+
    require '/var/www/confBD/bd.php';
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
+
 
−
     $conex->set_charset('utf8mb4');
+
    $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,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
     $stmt = $conex->query(CONSULTAR_ATRACCIONS);
 +
    $atraccions = $stmt->fetchAll();
  
−
    // Lóxica para dar de alta unha nova pista
+
     $stmt = $conex->query(CONSULTAR_PISTAS);
−
     $nombre = $conex->real_escape_string($_POST['nombre']);
+
     $pistas = $stmt->fetchAll();
−
    $aforo = $conex->real_escape_string($_POST['aforo']);
 
−
     $consulta = sprintf(INSERTAR_PISTAS,$nombre, $aforo);
 
−
    $conex->query($consulta);
 
  
−
  } catch (mysqli_sql_exception $e) {
+
} catch (PDOException $e) {
−
     die("Erro: " . $e->getMessage());
+
     error_log($e->getMessage());
−
  }
+
    die('Erro relacionado coa base de datos.');
−
  catch (Expcetion $e) {
+
} catch (Throwable $e) {
−
     die("Erro xeral: " . $e->getMessage());
+
     error_log($e->getMessage());
−
  }
+
     die('Produciuse un erro na aplicación.');
−
  finally{
 
−
     if(!empty($conex)){
 
−
      $conex->close();
 
−
    } 
 
−
  }
 
−
 
 
 
}
 
}
−
header("Location: listar_pistas.php");
+
?>
 +
<!DOCTYPE html>
 +
<html lang="gl">
 +
<head>
 +
    <meta charset="UTF-8">
 +
    <title>Alta de animal</title>
 +
</head>
 +
<body>
 +
 
 +
    <h1>Alta de animal</h1>
  
 +
    <form method="post" action="procesar_alta_animal.php">
 +
        <?php require 'campos_animal.php'; ?>
 +
        <button type="submit" name="btnAltaAnimal" value="ALTA">Dar de alta</button>
 +
    </form>
  
 +
</body>
 +
</html>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
 +
=== Validación dos datos opcionais ===
  
 +
Se un campo opcional chega como cadea baleira, podemos convertelo en <code>null</code>:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$nombrePista = trim(
 +
    $_POST['nombre_pista'] ?? ''
 +
);
 +
 +
$nombrePista = $nombrePista === ''
 +
    ? null
 +
    : $nombrePista;
 +
</syntaxhighlight>
 +
 +
Para un número opcional é mellor distinguir explicitamente a cadea baleira dun valor numérico:
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$anhosTexto = trim(
 +
    $_POST['anhos'] ?? ''
 +
);
 +
 +
if ($anhosTexto === '') {
 +
 +
    $anhos = null;
 +
 +
} else {
 +
 +
    $anhos = filter_var(
 +
        $anhosTexto,
 +
        FILTER_VALIDATE_INT,
 +
        [
 +
            'options' => [
 +
                'min_range' => 1,
 +
                'max_range' => 50
 +
            ]
 +
        ]
 +
    );
 +
}
 +
</syntaxhighlight>
 +
 +
Non empregaremos <code>empty()</code> como regra xeral para validar números porque tamén considera baleiros valores como <code>0</code> e <code>'0'</code>.
  
 
<br />
 
<br />
−
'''Arquivo pistas/editar_pista.php'''
+
 
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
=== Arquivo animais/procesar_alta_animal.php ===
 +
 
 +
::<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_pistas.inc.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,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 
 +
    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnAltaAnimal']) || $_POST['btnAltaAnimal'] !== 'ALTA') {
 +
        throw new RuntimeException('Operación non válida.');
 +
    }
 +
 
 +
    $nombre = trim($_POST['nombre'] ?? '');
 +
    $tipo = trim($_POST['tipo'] ?? '');
  
−
?>
+
    $tiposPermitidos = ['León', 'Cocodrilo', 'Jirafa', 'Mono'];
 +
 
 +
    if ($nombre === '' || !in_array($tipo, $tiposPermitidos, true)) {
 +
        header('Location: consultar_animais.php?error=' . urlencode('O nome ou o tipo non son válidos.'));
 +
        exit;
 +
    }
 +
 
 +
    $anhosTexto = trim($_POST['anhos'] ?? '');
 +
    $anhos = null;
 +
 
 +
    if ($anhosTexto !== '') {
 +
        $anhos = filter_var(
 +
            $anhosTexto,
 +
            FILTER_VALIDATE_INT,
 +
            ['options' => ['min_range' => 1, 'max_range' => 50]]
 +
        );
 +
 
 +
        if ($anhos === false) {
 +
            header('Location: consultar_animais.php?error=' . urlencode('O número de anos non é válido.'));
 +
            exit;
 +
        }
 +
    }
 +
 
 +
    $pesoTexto = trim($_POST['peso'] ?? '');
 +
    $peso = null;
 +
 
 +
    if ($pesoTexto !== '') {
 +
        $peso = filter_var($pesoTexto, FILTER_VALIDATE_FLOAT);
 +
 
 +
        if ($peso === false || $peso < 0.1 || $peso > 500) {
 +
            header('Location: consultar_animais.php?error=' . urlencode('O peso non é válido.'));
 +
            exit;
 +
        }
 +
    }
 +
 
 +
    $estaturaTexto = trim($_POST['estatura'] ?? '');
 +
    $estatura = null;
 +
 
 +
    if ($estaturaTexto !== '') {
 +
        $estatura = filter_var($estaturaTexto, FILTER_VALIDATE_FLOAT);
 +
 
 +
        if ($estatura === false || $estatura < 0.1 || $estatura > 20) {
 +
            header('Location: consultar_animais.php?error=' . urlencode('A estatura non é válida.'));
 +
            exit;
 +
        }
 +
    }
 +
 
 +
    $nombreAtraccion = trim($_POST['nombre_atraccion'] ?? '');
 +
    $nombreAtraccion = $nombreAtraccion === '' ? null : $nombreAtraccion;
 +
 
 +
    $nombrePista = trim($_POST['nombre_pista'] ?? '');
 +
    $nombrePista = $nombrePista === '' ? null : $nombrePista;
 +
 
 +
    $stmt = $conex->prepare(INSERIR_ANIMAL);
 +
 
 +
    $stmt->bindValue(':nombre', $nombre, PDO::PARAM_STR);
 +
    $stmt->bindValue(':tipo', $tipo, PDO::PARAM_STR);
 +
 
 +
    $stmt->bindValue(
 +
        ':anhos',
 +
        $anhos,
 +
        $anhos === null ? PDO::PARAM_NULL : PDO::PARAM_INT
 +
    );
 +
 
 +
    $stmt->bindValue(
 +
        ':peso',
 +
        $peso === null ? null : (string) $peso,  // $peso es double, pero no existe PARAM_DOUBLE en PDO polo que temos que envialo como String
 +
        $peso === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 
 +
    $stmt->bindValue(
 +
        ':estatura',
 +
        $estatura === null ? null : (string) $estatura,    // $estatura es double, pero no existe PARAM_DOUBLE en PDO polo que temos que envialo como String
 +
        $estatura === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 
 +
    $stmt->bindValue(
 +
        ':nombre_atraccion',
 +
        $nombreAtraccion,
 +
        $nombreAtraccion === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 
 +
    $stmt->bindValue(
 +
        ':nombre_pista',
 +
        $nombrePista,
 +
        $nombrePista === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 
 +
    $stmt->execute();
 +
 
 +
    header('Location: consultar_animais.php?msg=' . urlencode('Animal dado de alta correctamente.'));
 +
    exit;
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    $codigoMySQL = $e->errorInfo[1] ?? null;
 +
 
 +
    if ($codigoMySQL === 1062) {
 +
        $mensaxe = 'Xa existe un animal con ese nome.';
 +
    } elseif ($codigoMySQL === 1452) {
 +
        $mensaxe = 'A pista ou a atracción seleccionada non é válida.';
 +
    } else {
 +
        $mensaxe = 'Erro relacionado coa base de datos.';
 +
    }
 +
 
 +
    header('Location: consultar_animais.php?error=' . urlencode($mensaxe));
 +
    exit;
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    header('Location: consultar_animais.php?error=' . urlencode('Produciuse un erro na aplicación.'));
 +
    exit;
 +
}
 +
</syntaxhighlight>
 +
 
 +
PDO non dispón dun tipo <code>PDO::PARAM_FLOAT</code>. Neste exemplo os valores decimais non nulos envíanse como cadeas, mentres que os valores ausentes se ligan explicitamente como <code>PDO::PARAM_NULL</code>.
 +
 
 +
<br />
 +
 
 +
=== Arquivo animais/editar_animal.php ===
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
 
 +
try {
 +
 
 +
    require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
  
−
<?php
+
     $nombreRecibido = trim($_GET['nombre'] ?? '');
−
     $consulta = sprintf(CONSULTAR_PISTAS_POR_NOME,$conex->real_escape_string($_GET['nombre']));
 
−
    $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
 
−
        $nombre = htmlentities($row['nombre']);
 
−
        $aforo = htmlentities($row['aforo']);
 
  
−
         $result->free();
+
    if ($nombreRecibido === '') {
 +
         header('Location: consultar_animais.php?error=' . urlencode('Non se indicou un animal válido.'));
 +
        exit;
 
     }
 
     }
−
     else {
+
 
−
         $erros = "Non se atopan os datos da pista!!!!";
+
    $stmt = $conex->prepare(CONSULTAR_ANIMAL_POR_NOME);
 +
    $stmt->execute([':nombre' => $nombreRecibido]);
 +
    $animal = $stmt->fetch();
 +
 
 +
     if ($animal === false) {
 +
         header('Location: consultar_animais.php?error=' . urlencode('Non se atopou o animal indicado.'));
 +
        exit;
 
     }
 
     }
  
 +
    $nombre = $animal['nombre'];
 +
    $tipo = $animal['tipo'];
 +
    $anhos = $animal['anhos'];
 +
    $peso = $animal['peso'];
 +
    $estatura = $animal['estatura'];
 +
    $nombre_atraccion = $animal['nombre_atraccion'];
 +
    $nombre_pista = $animal['nombre_pista'];
  
 +
    $stmt = $conex->query(CONSULTAR_ATRACCIONS);
 +
    $atraccions = $stmt->fetchAll();
 +
 +
    $stmt = $conex->query(CONSULTAR_PISTAS);
 +
    $pistas = $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>
 
<!DOCTYPE html>
−
<html>
+
<html lang="gl">
 
<head>
 
<head>
−
  <title>Modificar Pista</title>
+
    <meta charset="UTF-8">
 +
    <title>Modificar animal</title>
 
</head>
 
</head>
−
<body>
+
<body>
−
  <h3>Modificar pista</h3>
+
 
 +
    <h1>Modificar animal</h1>
 +
 
 +
    <form method="post" action="procesar_editar_animal.php">
 +
        <?php require 'campos_animal.php'; ?>
 +
 
 +
        <input
 +
            type="hidden"
 +
            name="nome_orixinal"
 +
            value="<?= htmlspecialchars($nombre, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
 +
        >
 +
 
 +
        <button type="submit" name="btnModificarAnimal" value="MODIFICAR">Modificar</button>
 +
    </form>
  
−
  <form method="POST" action="procesar_editar_pista.php">
 
−
    <?php include 'campos_pista.php'; ?>
 
−
    <div>
 
−
        <input type="hidden" name="nome_orixinal" value="<?php echo $nombre; ?>">
 
−
        <input type="submit" value="Modificar" name="btnModificarPista">
 
−
    </div>
 
−
  </form>
 
−
  <?php
 
−
    if(!empty($erros)){
 
−
        printf("<h3>$erros</h3>");
 
−
    }
 
−
  ?>
 
 
</body>
 
</body>
 
</html>
 
</html>
 +
</syntaxhighlight>
  
 +
Os valores recuperados da base de datos mantéñense sen codificar nas variables. O escapado faise unicamente no momento de xerar a saída HTML, polo que as comparacións dos <code>select</code> funcionan correctamente aínda que existan acentos.
 +
 +
<br />
 +
 +
=== Arquivo animais/procesar_editar_animal.php ===
 +
 +
O tratamento dos campos opcionais é idéntico ao realizado na alta.
 +
 +
::<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,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
 +
 +
    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnModificarAnimal']) || $_POST['btnModificarAnimal'] !== 'MODIFICAR') {
 +
        throw new RuntimeException('Operación non válida.');
 +
    }
 +
 +
    $nombreNovo = trim($_POST['nombre'] ?? '');
 +
    $nombreOrixinal = trim($_POST['nome_orixinal'] ?? '');
 +
    $tipo = trim($_POST['tipo'] ?? '');
 +
 +
    $tiposPermitidos = ['León', 'Cocodrilo', 'Jirafa', 'Mono'];
 +
 +
    if ($nombreNovo === '' || $nombreOrixinal === '' || !in_array($tipo, $tiposPermitidos, true)) {
 +
        header('Location: consultar_animais.php?error=' . urlencode('O nome ou o tipo non son válidos.'));
 +
        exit;
 +
    }
 +
 +
    $anhosTexto = trim($_POST['anhos'] ?? '');
 +
    $anhos = null;
 +
 +
    if ($anhosTexto !== '') {
 +
        $anhos = filter_var(
 +
            $anhosTexto,
 +
            FILTER_VALIDATE_INT,
 +
            ['options' => ['min_range' => 1, 'max_range' => 50]]
 +
        );
 +
 +
        if ($anhos === false) {
 +
            header('Location: consultar_animais.php?error=' . urlencode('O número de anos non é válido.'));
 +
            exit;
 +
        }
 +
    }
 +
 +
    $pesoTexto = trim($_POST['peso'] ?? '');
 +
    $peso = null;
 +
 +
    if ($pesoTexto !== '') {
 +
        $peso = filter_var($pesoTexto, FILTER_VALIDATE_FLOAT);
 +
 +
        if ($peso === false || $peso < 0.1 || $peso > 500) {
 +
            header('Location: consultar_animais.php?error=' . urlencode('O peso non é válido.'));
 +
            exit;
 +
        }
 +
    }
 +
 +
    $estaturaTexto = trim($_POST['estatura'] ?? '');
 +
    $estatura = null;
 +
 +
    if ($estaturaTexto !== '') {
 +
        $estatura = filter_var($estaturaTexto, FILTER_VALIDATE_FLOAT);
 +
 +
        if ($estatura === false || $estatura < 0.1 || $estatura > 20) {
 +
            header('Location: consultar_animais.php?error=' . urlencode('A estatura non é válida.'));
 +
            exit;
 +
        }
 +
    }
 +
 +
    $nombreAtraccion = trim($_POST['nombre_atraccion'] ?? '');
 +
    $nombreAtraccion = $nombreAtraccion === '' ? null : $nombreAtraccion;
 +
 +
    $nombrePista = trim($_POST['nombre_pista'] ?? '');
 +
    $nombrePista = $nombrePista === '' ? null : $nombrePista;
 +
 +
    $stmt = $conex->prepare(ACTUALIZAR_ANIMAL);
 +
 +
    $stmt->bindValue(':nombre_novo', $nombreNovo, PDO::PARAM_STR);
 +
    $stmt->bindValue(':tipo', $tipo, PDO::PARAM_STR);
 +
 +
    $stmt->bindValue(
 +
        ':anhos',
 +
        $anhos,
 +
        $anhos === null ? PDO::PARAM_NULL : PDO::PARAM_INT
 +
    );
 +
 +
    $stmt->bindValue(
 +
        ':peso',
 +
        $peso === null ? null : (string) $peso,
 +
        $peso === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 +
    $stmt->bindValue(
 +
        ':estatura',
 +
        $estatura === null ? null : (string) $estatura,
 +
        $estatura === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 +
    $stmt->bindValue(
 +
        ':nombre_atraccion',
 +
        $nombreAtraccion,
 +
        $nombreAtraccion === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 +
    $stmt->bindValue(
 +
        ':nombre_pista',
 +
        $nombrePista,
 +
        $nombrePista === null ? PDO::PARAM_NULL : PDO::PARAM_STR
 +
    );
 +
 +
    $stmt->bindValue(':nombre_orixinal', $nombreOrixinal, PDO::PARAM_STR);
 +
 +
    $stmt->execute();
 +
 +
    $mensaxe = $stmt->rowCount() === 1
 +
        ? 'Animal actualizado correctamente.'
 +
        : 'Non se modificou ningunha fila (pode non existir ou non haber cambios).';
 +
 +
    header('Location: consultar_animais.php?msg=' . urlencode($mensaxe));
 +
    exit;
 +
 +
} catch (PDOException $e) {
 +
 +
    error_log($e->getMessage());
 +
    $codigoMySQL = $e->errorInfo[1] ?? null;
 +
 +
    if ($codigoMySQL === 1062) {
 +
        $mensaxe = 'Xa existe un animal co novo nome indicado.';
 +
    } elseif ($codigoMySQL === 1452) {
 +
        $mensaxe = 'A pista ou a atracción seleccionada non é válida.';
 +
    } else {
 +
        $mensaxe = 'Erro relacionado coa base de datos.';
 +
    }
 +
 +
    header('Location: consultar_animais.php?error=' . urlencode($mensaxe));
 +
    exit;
 +
 +
} catch (Throwable $e) {
 +
 +
    error_log($e->getMessage());
 +
    header('Location: consultar_animais.php?error=' . urlencode('Produciuse un erro na aplicación.'));
 +
    exit;
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
 +
 +
=== Arquivo animais/procesar_borrar_animal.php ===
 +
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
 +
try {
 +
 +
    require '/var/www/confBD/bd.php';
 +
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 +
            PDO::ATTR_EMULATE_PREPARES => false
 +
        ]
 +
    );
  
 +
    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnBorrarAnimal']) || $_POST['btnBorrarAnimal'] !== 'BORRAR') {
 +
        throw new RuntimeException('Operación non válida.');
 +
    }
 +
 +
    $nombre = trim($_POST['nombre'] ?? '');
 +
 +
    if ($nombre === '') {
 +
        header('Location: consultar_animais.php?error=' . urlencode('Non se indicou un animal válido.'));
 +
        exit;
 +
    }
 +
 +
    $stmt = $conex->prepare(BORRAR_ANIMAL);
 +
    $stmt->execute([':nombre' => $nombre]);
 +
 +
    $mensaxe = $stmt->rowCount() === 1
 +
        ? 'Animal borrado correctamente.'
 +
        : 'Non se atopou o animal que se quería borrar.';
 +
 +
    header('Location: consultar_animais.php?msg=' . urlencode($mensaxe));
 +
    exit;
 +
 +
} catch (PDOException $e) {
 +
 +
    error_log($e->getMessage());
 +
    $codigoMySQL = $e->errorInfo[1] ?? null;
 +
 +
    $mensaxe = $codigoMySQL === 1451
 +
        ? 'O animal está relacionado con outros datos e non se pode borrar.'
 +
        : 'Erro relacionado coa base de datos.';
 +
 +
    header('Location: consultar_animais.php?error=' . urlencode($mensaxe));
 +
    exit;
 +
 +
} catch (Throwable $e) {
 +
 +
    error_log($e->getMessage());
 +
    header('Location: consultar_animais.php?error=' . urlencode('Produciuse un erro na aplicación.'));
 +
    exit;
 +
}
 +
</syntaxhighlight>
  
 
<br />
 
<br />
−
'''Arquivo pistas/procesar_editar_pista.php'''
+
 
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
== mysqli como ampliación ==
 +
 
 +
PDO é o mecanismo principal empregado nestes apuntes. A extensión <code>mysqli</code> tamén permite realizar estas operacións correctamente, pero empregaremos igualmente consultas preparadas.
 +
 
 +
=== Conexión ===
 +
 
 +
::<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_pistas.inc.php');
+
try {
−
$conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
+
 
−
$conex->set_charset('utf8mb4');
+
    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...
  
−
if (isset($_POST['btnModificarPista'])) {
+
} catch (mysqli_sql_exception $e) {
−
  try {
 
  
−
     // Lóxica para dar de alta unha nova pista
+
     error_log($e->getMessage());
−
    $nome_novo = $conex->real_escape_string($_POST['nombre']);
+
     die('Erro relacionado coa base de datos.');
−
    $nome_orixinal = $conex->real_escape_string($_POST['nome_orixinal']); // Ven nun campo oculto do form
 
−
     $aforo = $conex->real_escape_string($_POST['aforo']);
 
−
    $consulta = sprintf(ACTUALIZAR_PISTAS,$nome_novo, $aforo,$nome_orixinal);
 
−
    $conex->query($consulta);
 
  
−
  } catch (mysqli_sql_exception $e) {
+
} catch (Throwable $e) {
−
     die("Erro: " . $e->getMessage());
+
 
−
  }
+
     error_log($e->getMessage());
−
  catch (Expcetion $e) {
+
     die('Produciuse un erro na aplicación.');
−
     die("Erro xeral: " . $e->getMessage());
 
−
  }
 
−
  finally{
 
−
    if(!empty($conex)){
 
−
      $conex->close();
 
−
    } 
 
−
  }
 
 
}
 
}
−
header("Location: listar_pistas.php");
+
</syntaxhighlight>
 +
 
 +
=== INSERT preparado ===
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'INSERT INTO PISTAS (nombre, aforo)
 +
    VALUES (?, ?)'
 +
);
 +
 
 +
$stmt->bind_param(
 +
    'si',
 +
    $nombre,
 +
    $aforo
 +
);
 +
 
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
 
 +
=== SELECT preparado ===
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'SELECT nombre, aforo
 +
    FROM PISTAS
 +
    WHERE nombre = ?'
 +
);
  
−
?>
+
$stmt->bind_param(
 +
    's',
 +
    $nombre
 +
);
 +
 
 +
$stmt->execute();
 +
$resultado = $stmt->get_result();
 +
$pista = $resultado->fetch_assoc();
 +
</syntaxhighlight>
 +
 
 +
=== UPDATE preparado ===
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'UPDATE PISTAS
 +
    SET nombre = ?,
 +
        aforo = ?
 +
    WHERE nombre = ?'
 +
);
 +
 
 +
$stmt->bind_param(
 +
    'sis',
 +
    $nombreNovo,
 +
    $aforo,
 +
    $nombreOrixinal
 +
);
 +
 
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
 
 +
=== DELETE preparado ===
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'DELETE FROM PISTAS
 +
    WHERE nombre = ?'
 +
);
 +
 
 +
$stmt->bind_param(
 +
    's',
 +
    $nombre
 +
);
 +
 
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
 
 +
Non empregaremos como patrón principal <code>real_escape_string()</code> combinado con <code>sprintf()</code> para introducir valores nunha orde SQL.
 +
 
 +
<br />
 +
 
 +
== Exercicios propostos ==
 +
 
 +
Se realizaches os exercicios anteriores xa tes unha estrutura que podes reutilizar para o mantemento doutras táboas.
 +
 
 +
Podes escoller outra táboa e implementar listado, alta, modificación, borrado, validación no servidor, consultas preparadas e tratamento de erros.
 +
 
 +
<br />
 +
 
 +
=== Exercicio con autores e libros ===
 +
 
 +
Dado o nome e apelidos dun autor seleccionado nun elemento <code>&lt;select&gt;</code>, amosar nunha táboa a lista de títulos escritos polo autor, ordenados polo título.
 +
 
 +
Se o autor non ten libros deberá aparecer:
  
 +
::<syntaxhighlight lang="text" enclose="div">
 +
Sen libros escritos
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
O identificador do autor debe enviarse mediante unha consulta preparada.
 +
 +
<br />
 +
 +
=== Listado de libros con paxinación ===
  
 +
Amosar os libros con paxinación. Asociar a cada libro un elemento que permita seleccionalo e, ao realizar a selección, amosar os exemplares dese libro coa imaxe, prezo e estado.
  
 
<br />
 
<br />
−
'''Arquivo pistas/procesar_borrar_pista.php'''
 
−
::<syntaxhighlight lang="php" line 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_pistas.inc.php');
+
=== Busca de libros ===
−
if (isset($_POST['btnBorrarPista'])) {
+
 
−
  try {
+
Dispoñer dunha caixa de texto na que o usuario escriba parte do título dun libro.
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
+
 
−
    $conex->set_charset('utf8mb4');
+
Realizar unha consulta con <code>LIKE</code> e amosar os libros que coincidan co patrón xunto co número de exemplares de cada libro.
  
−
    // Lóxica para dar de alta unha nova pista
+
O patrón constrúese como valor PHP:
−
    $nombre = $conex->real_escape_string($_POST['nombre']);
 
−
    $consulta = sprintf(BORRAR_PISTAS,$nombre);
 
−
    $conex->query($consulta);
 
  
−
  } catch (mysqli_sql_exception $e) {
+
::<syntaxhighlight lang="php" enclose="div">
−
    die("Erro: " . $e->getMessage());
+
$patron = '%' . $texto . '%';
−
  }
 
−
  catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
  }
 
−
  finally{
 
−
    if(!empty($conex)){
 
−
      $conex->close();
 
−
    } 
 
−
  }
 
−
 
 
−
}
 
−
header("Location: listar_pistas.php");
 
  
 +
$stmt = $conex->prepare(
 +
    'SELECT ...
 +
    WHERE titulo LIKE :patron'
 +
);
  
 +
$stmt->execute([
 +
    ':patron' => $patron
 +
]);
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Os caracteres <code>%</code> forman parte do valor do parámetro; a entrada do usuario non se concatena dentro da orde SQL.
 +
 +
<br />
 +
 +
=== Mantemento dunha relación N:M ===
  
 +
Nunha relación N:M podemos seleccionar un elemento da primeira táboa, consultar todos os elementos da segunda e amosar un <code>checkbox</code> por cada un deles.
  
 +
Por exemplo:
  
 +
:* seleccionar unha atracción;
 +
:* amosar todos os artistas;
 +
:* marcar os artistas que traballan nela;
 +
:* permitir engadir ou eliminar asociacións.
  
−
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
+
Como unha única acción do usuario pode provocar varios <code>INSERT</code> e <code>DELETE</code>, este tipo de operación é un bo exemplo para empregar unha '''transacción''':
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$conex->beginTransaction();
 +
 
 +
try {
 +
 
 +
    // DELETE das relacións eliminadas.
 +
    // INSERT das novas relacións.
 +
 
 +
    $conex->commit();
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    if ($conex->inTransaction()) {
 +
        $conex->rollBack();
 +
    }
 +
 
 +
    throw $e;
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Deste modo todas as modificacións se confirman xuntas ou se desfán se se produce un erro.
 +
 +
<br />
 +
 +
== Resumo ==
 +
 +
* Empregaremos '''PDO como mecanismo principal''' de acceso á base de datos.
 +
* O arquivo <code>/var/www/confBD/bd.php</code> queda fóra de <code>/var/www/html</code>.
 +
* As credenciais e as consultas estarán centralizadas nese arquivo.
 +
* As consultas con datos variables realizaranse con <code>prepare()</code>.
 +
* Non introduciremos datos do usuario na SQL mediante <code>sprintf()</code>, concatenación ou interpolación.
 +
* Os datos validaranse sempre no servidor.
 +
* O escapado HTML realizarase no momento de xerar a saída mediante <code>htmlspecialchars()</code>.
 +
* Non gardaremos entidades HTML na base de datos como mecanismo xeral de seguridade.
 +
* As operacións que modifican datos empregarán <code>POST</code>.
 +
* A protección CSRF incorporarase cando se estuden as sesións.
 +
* Os campos <code>hidden</code> poden ser manipulados e deben tratarse como datos non confiables.
 +
* As claves primarias modificables requiren conservar o valor orixinal para localizar a fila.
 +
* Os conflitos de clave duplicada deben estar garantidos pola base de datos e tratados pola aplicación.
 +
* Os valores opcionais representaranse en PHP mediante <code>null</code>.
 +
* Con PDO empregaremos <code>PDO::PARAM_NULL</code> cando necesitemos ligar explicitamente un valor SQL <code>NULL</code>.
 +
* Non construiremos manualmente a cadea <code>'null'</code> nin faremos <code>str_replace()</code> sobre a consulta.
 +
* Os valores usados para decidir unha opción <code>selected</code> compararanse na súa forma orixinal.
 +
* <code>htmlspecialchars()</code> aplicarase só cando xeremos o atributo ou texto HTML correspondente.
 +
* Os erros técnicos rexistraranse con <code>error_log()</code>.
 +
* Non mostraremos directamente <code>$e->getMessage()</code> ao usuario.
 +
* <code>mysqli</code> queda como ampliación, tamén empregando consultas preparadas.
 +
 +
 +
 +
 +
== Comprobación final dos exercicios ==
 +
 +
Antes de dar por rematada a práctica comprobaremos que:
 +
 +
# As listaxes funcionan sen parámetros e as consultas dunha fila empregan <code>prepare()</code> e <code>execute()</code>.
 +
# O alta valida os campos no servidor, insire mediante <code>POST</code> e trata as claves duplicadas.
 +
# A modificación conserva a clave primaria orixinal e permite cambiar o nome da pista ou do animal.
 +
# O borrado só se realiza mediante <code>POST</code> e trata posibles conflitos de integridade referencial.
 +
# Os campos opcionais baleiros gárdanse como <code>NULL</code> cando corresponde e non como texto <code>'null'</code>.
 +
# As etiquetas, textos e atributos HTML amosan os datos mediante <code>htmlspecialchars()</code>.
 +
# Non se mostran credenciais nin mensaxes técnicas de excepción no navegador.
 +
# As probas inclúen entradas mal formadas, valores límite e rexistros inexistentes.
 +
 +
 +
 +
 +
<br />
 +
== Ampliación dos exercicios ==
  
−
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2023).
+
* Realizar unha segunda versión dos exercicios anteriores nas páxinas de 'procesar....' para que se chame a un procedemento almacenado de Mysql.
 +
: Define a chamada ao procedemento o os parámetros necesarios no arquivo bd.php.

Revisión actual del 10:33 8 oct 2026

Introdución

Nesta unidade propoñemos varios exercicios completos de mantemento de datos empregando PHP 8.5 e un servidor MySQL 8.0 ou 8.4.

O obxectivo é practicar as operacións fundamentais dun CRUD:

  • Create → INSERT.
  • Read → SELECT.
  • Update → UPDATE.
  • Delete → DELETE.

Nos exemplos completos empregaremos PDO (PHP Data Objects) como mecanismo principal de acceso á base de datos.

Ao final inclúese unha sección comparativa con mysqli. Nos exercicios e prácticas deste módulo empregarase PDO salvo indicación expresa en contrario, seguindo os apuntes de operacións contra MySQL.


  • NOTA IMPORTANTE: cando empregamos datos procedentes dun formulario debemos:
  • validar os datos recibidos no servidor;
  • empregar consultas preparadas cando unha orde SQL conteña datos variables;
  • escapar os datos no momento de xerar a saída HTML;
  • non mostrar ao usuario os detalles técnicos das excepcións;
  • empregar POST para as operacións que modifican datos;
  • comprobar os permisos ou autorización do usuario nunha aplicación real.


A protección fronte a ataques CSRF (Cross-Site Request Forgery) incorporarase cando se estuden as variables de sesións. Empregar POST non evita por si só un ataque CSRF.


Consideracións sobre os exercicios

  • Todos os exercicios están baseados na base de datos CIRCO empregada noutro módulo.
Mod BD Prog Modelo Circo.jpg
  • A base de datos ten algunhas características pouco habituais nunha aplicación real porque está deseñada tamén para practicar SQL.

Por exemplo, nalgunhas táboas as claves primarias son cadeas visibles para o usuario:

PISTAS.nombre
ANIMALES.nombre

Isto implica que, nunha modificación, o usuario pode cambiar o valor da clave primaria.

Por tanto teremos que conservar:

  • o valor orixinal da clave primaria, para localizar a fila;
  • o novo valor introducido polo usuario, para actualizar a fila.

Nunha aplicación habitual é frecuente empregar como clave primaria un identificador numérico autonumérico que o usuario non modifica.


  • Se o usuario modifica unha clave primaria e o novo valor xa existe, a base de datos producirá un erro de clave duplicada.

Podemos tratar esta situación comprobando previamente se o novo valor existe ou deixando que a restrición da base de datos detecte o conflito e capturando a excepción. A restrición da base de datos debe existir igualmente.


  • O usuario empregado pola aplicación debe ter unicamente os permisos necesarios. Para estes exercicios serán normalmente SELECT, INSERT, UPDATE e DELETE. O permiso EXECUTE só será necesario se tamén se practican procedementos almacenados.
  • Os arquivos PHP estarán dentro da estrutura do sitio web, pero o arquivo coas credenciais e consultas estará fóra de /var/www/html.

Empregaremos:

/var/www/confBD/bd.php


  • PASOS A SEGUIR E CONSIDERACIÓNS:
  • Importar a base de datos CIRCO en MySQL 8 e comprobar a estrutura e as restricións das táboas.
  • Crear un usuario específico con permisos mínimos (SELECT, INSERT, UPDATE e DELETE) sobre as táboas necesarias. Non empregar root na aplicación.
  • Gardar as credenciais e as constantes SQL en /var/www/confBD/bd.php, fóra do directorio público do sitio web.
  • Durante o desenvolvemento consultar os erros no rexistro do servidor e empregar un depurador cando sexa posible. A opción display_errors non debe estar activa en produción.
  • Configurar os permisos de forma que o usuario do servidor web poida ler os scripts e o arquivo de configuración, sen conceder escritura onde non sexa necesaria. Evitar que outros usuarios locais poidan ler as credenciais.
  • Manter separados os formularios HTML e os scripts de procesamento de INSERT, UPDATE e DELETE.
  • As consultas preparadas non deben reconstruírse concatenando datos do formulario; os valores variables enlázanse con parámetros.


Para todos os exercicios

Arquivo /var/www/confBD/bd.php

O arquivo terá inicialmente:

<?php

// Arquivo de configuración fóra do directorio público.
define('SERVIDOR', 'localhost');
define('USUARIO', 'user_php');
define('PASSWORD', 'user_php');
define('BD', 'CIRCO');

Neste mesmo arquivo iremos engadindo as constantes coas consultas necesarias para os diferentes exercicios.


Esquema común para acceder á base de datos

Nas páxinas PHP que accedan á base de datos empregaremos 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,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );

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


Consultas con e sen parámetros

Se unha consulta non ten valores variables, podemos utilizar directamente query():

$stmt = $conex->query(CONSULTAR_PISTAS);
$pistas = $stmt->fetchAll();

Se a consulta contén valores variables, empregaremos prepare() e execute():

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

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

$pista = $stmt->fetch();

Non construiremos as consultas inserindo directamente os datos mediante sprintf(), concatenación ou interpolación.


Saída HTML

Os datos gardaranse normalmente na base de datos na súa forma orixinal.

Aplicaremos htmlspecialchars() cando os introduzamos nun documento HTML:

<?= htmlspecialchars(
    $pista['nombre'],
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
) ?>

Para un valor que pode ser NULL podemos facer:

<?= htmlspecialchars(
    (string) ($animal['nombre_pista'] ?? ''),
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
) ?>

Deste xeito NULL represéntase como unha cadea baleira na saída HTML.


Exercicio 1: mantemento da táboa PISTAS

Partindo da táboa PISTAS, crea unha aplicación que permita listar, dar de alta, modificar e borrar pistas.

Crearemos o cartafol:

pistas/

coas seguintes páxinas:

consultar_pistas.php
alta_pista.php
campos_pista.php
procesar_alta_pista.php
editar_pista.php
procesar_editar_pista.php
procesar_borrar_pista.php


Consultas necesarias

Engadimos ao arquivo /var/www/confBD/bd.php:

define(
    'CONSULTAR_PISTAS',
    'SELECT nombre, aforo
     FROM PISTAS
     ORDER BY nombre'
);

define(
    'CONSULTAR_PISTA_POR_NOME',
    'SELECT nombre, aforo
     FROM PISTAS
     WHERE nombre = :nombre'
);

define(
    'INSERIR_PISTA',
    'INSERT INTO PISTAS (nombre, aforo)
     VALUES (:nombre, :aforo)'
);

define(
    'ACTUALIZAR_PISTA',
    'UPDATE PISTAS
     SET nombre = :nombre_novo,
         aforo = :aforo
     WHERE nombre = :nombre_orixinal'
);

define(
    'BORRAR_PISTA',
    'DELETE FROM PISTAS
     WHERE nombre = :nombre'
);


Arquivo pistas/consultar_pistas.php

<?php

try {

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

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

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

    $stmt = $conex->query(CONSULTAR_PISTAS);
    $pistas = $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 pistas</title>
</head>
<body>

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

    <form action="alta_pista.php" method="get">
        <button type="submit">Nova pista</button>
    </form>

    <table>
        <thead>
            <tr>
                <th>Nome</th>
                <th>Aforo</th>
                <th>Accións</th>
            </tr>
        </thead>
        <tbody>

        <?php foreach ($pistas as $pista): ?>

            <tr>
                <td><?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars((string) $pista['aforo'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td>
                    <a href="editar_pista.php?nombre=<?= urlencode($pista['nombre']) ?>">Editar</a>

                    <form
                        action="procesar_borrar_pista.php"
                        method="post"
                        style="display:inline"
                        onsubmit="return confirm('Estás seguro de que queres borrar esta pista?');"
                    >
                        <input
                            type="hidden"
                            name="nombre"
                            value="<?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
                        >
                        <button type="submit" name="btnBorrarPista" 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>


Arquivo pistas/campos_pista.php

<label for="nombre">Nome:</label>
<input
    type="text"
    id="nombre"
    name="nombre"
    maxlength="50"
    required
    value="<?= htmlspecialchars($nombre ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
>
<br>

<label for="aforo">Aforo:</label>
<input
    type="number"
    id="aforo"
    name="aforo"
    min="20"
    max="500"
    required
    value="<?= htmlspecialchars(isset($aforo) ? (string) $aforo : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
>

A validación HTML mellora a experiencia do usuario, pero os mesmos criterios deben comprobarse novamente no servidor.


Arquivo pistas/alta_pista.php

<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Alta de pista</title>
</head>
<body>

    <h1>Alta de pista</h1>

    <form method="post" action="procesar_alta_pista.php">
        <?php require 'campos_pista.php'; ?>
        <div>
            <button type="submit" name="btnAltaPista" value="ALTA">Dar de alta</button>
        </div>
    </form>

</body>
</html>


Arquivo pistas/procesar_alta_pista.php

<?php

try {

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

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

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

    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnAltaPista']) || $_POST['btnAltaPista'] !== 'ALTA') {
        throw new RuntimeException('Operación non válida.');
    }

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

    $aforo = filter_input(
        INPUT_POST,
        'aforo',
        FILTER_VALIDATE_INT,
        [
            'options' => [
                'min_range' => 20,
                'max_range' => 500
            ]
        ]
    );

    if ($nombre === '' || mb_strlen($nombre, 'UTF-8') > 50) {
        header('Location: consultar_pistas.php?error=' . urlencode('O nome da pista non é válido.'));
        exit;
    }

    if ($aforo === false || $aforo === null) {
        header('Location: consultar_pistas.php?error=' . urlencode('O aforo non é válido.'));
        exit;
    }

    $stmt = $conex->prepare(INSERIR_PISTA);
    $stmt->bindValue(':nombre', $nombre, PDO::PARAM_STR);
    $stmt->bindValue(':aforo', $aforo, PDO::PARAM_INT);
    $stmt->execute();

    header('Location: consultar_pistas.php?msg=' . urlencode('Pista dada de alta correctamente.'));
    exit;

} catch (PDOException $e) {

    error_log($e->getMessage());
    $codigoMySQL = $e->errorInfo[1] ?? null;

    $mensaxe = $codigoMySQL === 1062
        ? 'Xa existe unha pista con ese nome.'
        : 'Erro relacionado coa base de datos.';

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

} catch (Throwable $e) {

    error_log($e->getMessage());
    header('Location: consultar_pistas.php?error=' . urlencode('Produciuse un erro na aplicación.'));
    exit;
}


Arquivo pistas/editar_pista.php

Esta páxina recibe o nome da pista mediante GET. É correcto porque a petición unicamente consulta 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,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );

    $nombreRecibido = trim($_GET['nombre'] ?? '');

    if ($nombreRecibido === '') {
        header('Location: consultar_pistas.php?error=' . urlencode('Non se indicou unha pista válida.'));
        exit;
    }

    $stmt = $conex->prepare(CONSULTAR_PISTA_POR_NOME);
    $stmt->execute([':nombre' => $nombreRecibido]);
    $pista = $stmt->fetch();

    if ($pista === false) {
        header('Location: consultar_pistas.php?error=' . urlencode('Non se atopou a pista indicada.'));
        exit;
    }

    $nombre = $pista['nombre'];
    $aforo = $pista['aforo'];

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

    <h1>Modificar pista</h1>

    <form method="post" action="procesar_editar_pista.php">
        <?php require 'campos_pista.php'; ?>

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

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

</body>
</html>

Fixarse en que gardamos o nome orixinal nun campo oculto porque o usuario pode modificar PISTAS.nombre, que neste modelo é a clave primaria. Un campo hidden pode ser manipulado, polo que o servidor segue tratando o valor como un dato non confiable.


Arquivo pistas/procesar_editar_pista.php

<?php

try {

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

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

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

    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnModificarPista']) || $_POST['btnModificarPista'] !== 'MODIFICAR') {
        throw new RuntimeException('Operación non válida.');
    }

    $nombreNovo = trim($_POST['nombre'] ?? '');
    $nombreOrixinal = trim($_POST['nome_orixinal'] ?? '');

    $aforo = filter_input(
        INPUT_POST,
        'aforo',
        FILTER_VALIDATE_INT,
        [
            'options' => [
                'min_range' => 20,
                'max_range' => 500
            ]
        ]
    );

    if ($nombreNovo === '' || mb_strlen($nombreNovo, 'UTF-8') > 50 || $nombreOrixinal === '') {
        header('Location: consultar_pistas.php?error=' . urlencode('Os datos da pista non son válidos.'));
        exit;
    }

    if ($aforo === false || $aforo === null) {
        header('Location: consultar_pistas.php?error=' . urlencode('O aforo non é válido.'));
        exit;
    }

    $stmt = $conex->prepare(ACTUALIZAR_PISTA);
    $stmt->bindValue(':nombre_novo', $nombreNovo, PDO::PARAM_STR);
    $stmt->bindValue(':aforo', $aforo, PDO::PARAM_INT);
    $stmt->bindValue(':nombre_orixinal', $nombreOrixinal, PDO::PARAM_STR);
    $stmt->execute();

    $mensaxe = $stmt->rowCount() === 1
        ? 'Pista actualizada correctamente.'
        : 'Non se modificou ningunha fila (pode non existir ou non haber cambios).';

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

} catch (PDOException $e) {

    error_log($e->getMessage());
    $codigoMySQL = $e->errorInfo[1] ?? null;

    $mensaxe = $codigoMySQL === 1062
        ? 'Xa existe unha pista co novo nome indicado.'
        : 'Erro relacionado coa base de datos.';

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

} catch (Throwable $e) {

    error_log($e->getMessage());
    header('Location: consultar_pistas.php?error=' . urlencode('Produciuse un erro na aplicación.'));
    exit;
}


Arquivo pistas/procesar_borrar_pista.php

<?php

try {

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

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

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

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

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

    if ($nombre === '') {
        header('Location: consultar_pistas.php?error=' . urlencode('Non se indicou unha pista válida.'));
        exit;
    }

    $stmt = $conex->prepare(BORRAR_PISTA);
    $stmt->execute([':nombre' => $nombre]);

    $mensaxe = $stmt->rowCount() === 1
        ? 'Pista borrada correctamente.'
        : 'Non se atopou a pista que se quería borrar.';

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

} catch (PDOException $e) {

    error_log($e->getMessage());
    $codigoMySQL = $e->errorInfo[1] ?? null;

    $mensaxe = $codigoMySQL === 1451
        ? 'A pista está relacionada con outros datos e non se pode borrar.'
        : 'Erro relacionado coa base de datos.';

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

} catch (Throwable $e) {

    error_log($e->getMessage());
    header('Location: consultar_pistas.php?error=' . urlencode('Produciuse un erro na aplicación.'));
    exit;
}


Exercicio 2: mantemento da táboa ANIMALES

A táboa ANIMALES incorpora varias características novas:

  • relacións con PISTAS e ATRACCIONES;
  • campos que poden conter NULL;
  • campos numéricos opcionais;
  • listas <select> cuxas opcións se obteñen da base de datos;
  • unha clave primaria textual que pode ser modificada polo usuario.

Os campos empregados no exercicio son:

nombre
tipo
anhos
peso
estatura
nombre_atraccion
nombre_pista

Os campos opcionais son:

anhos
peso
estatura
nombre_atraccion
nombre_pista

Crearemos o cartafol:

animais/

coas seguintes páxinas:

consultar_animais.php
alta_animal.php
campos_animal.php
procesar_alta_animal.php
editar_animal.php
procesar_editar_animal.php
procesar_borrar_animal.php


Valores NULL con PDO

Con PDO en PHP representaremos a ausencia de valor mediante null e enlazaremos explicitamente o parámetro como PDO::PARAM_NULL cando sexa necesario:

$stmt->bindValue(
    ':anhos',
    null,
    PDO::PARAM_NULL
);

Deste xeito a base de datos recibe un valor SQL NULL, non a cadea de texto 'null'.


Consultas necesarias

Engadimos ao arquivo /var/www/confBD/bd.php:

define(
    'CONSULTAR_ATRACCIONS',
    'SELECT nombre, fecha_inicio, ganancias
     FROM ATRACCIONES
     ORDER BY nombre'
);

define(
    'CONSULTAR_ANIMAIS',
    'SELECT
         ANIMALES.nombre,
         ANIMALES.tipo,
         ANIMALES.anhos,
         ANIMALES.peso,
         ANIMALES.estatura,
         ANIMALES.nombre_atraccion,
         ATRACCIONES.fecha_inicio,
         ANIMALES.nombre_pista,
         PISTAS.aforo
     FROM ANIMALES
     LEFT JOIN ATRACCIONES
         ON ANIMALES.nombre_atraccion = ATRACCIONES.nombre
     LEFT JOIN PISTAS
         ON ANIMALES.nombre_pista = PISTAS.nombre
     ORDER BY ANIMALES.nombre'
);

define(
    'CONSULTAR_ANIMAL_POR_NOME',
    'SELECT
         nombre,
         tipo,
         anhos,
         peso,
         estatura,
         nombre_atraccion,
         nombre_pista
     FROM ANIMALES
     WHERE nombre = :nombre'
);

define(
    'INSERIR_ANIMAL',
    'INSERT INTO ANIMALES (
         nombre,
         tipo,
         anhos,
         peso,
         estatura,
         nombre_atraccion,
         nombre_pista
     ) VALUES (
         :nombre,
         :tipo,
         :anhos,
         :peso,
         :estatura,
         :nombre_atraccion,
         :nombre_pista
     )'
);

define(
    'ACTUALIZAR_ANIMAL',
    'UPDATE ANIMALES
     SET nombre = :nombre_novo,
         tipo = :tipo,
         anhos = :anhos,
         peso = :peso,
         estatura = :estatura,
         nombre_atraccion = :nombre_atraccion,
         nombre_pista = :nombre_pista
     WHERE nombre = :nombre_orixinal'
);

define(
    'BORRAR_ANIMAL',
    'DELETE FROM ANIMALES
     WHERE nombre = :nombre'
);

Reutilizaremos tamén CONSULTAR_PISTAS, definida no exercicio anterior.


Arquivo animais/consultar_animais.php

<?php

try {

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

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

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

    $stmt = $conex->query(CONSULTAR_ANIMAIS);
    $animais = $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 animais</title>
</head>
<body>

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

    <form action="alta_animal.php" method="get">
        <button type="submit">Novo animal</button>
    </form>

    <table>
        <thead>
            <tr>
                <th>Nome</th>
                <th>Tipo</th>
                <th>Anos</th>
                <th>Peso</th>
                <th>Estatura</th>
                <th>Atracción</th>
                <th>Data inicio</th>
                <th>Pista</th>
                <th>Aforo</th>
                <th>Accións</th>
            </tr>
        </thead>
        <tbody>

        <?php foreach ($animais as $animal): ?>

            <?php
                $fechaInicio = '';

                if ($animal['fecha_inicio'] !== null) {
                    $fechaInicio = (new DateTime($animal['fecha_inicio']))->format('d-m-Y');
                }
            ?>

            <tr>
                <td><?= htmlspecialchars($animal['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars($animal['tipo'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars((string) ($animal['anhos'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars((string) ($animal['peso'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars((string) ($animal['estatura'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars((string) ($animal['nombre_atraccion'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars($fechaInicio, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars((string) ($animal['nombre_pista'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td><?= htmlspecialchars((string) ($animal['aforo'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
                <td>
                    <a href="editar_animal.php?nombre=<?= urlencode($animal['nombre']) ?>">Editar</a>

                    <form
                        action="procesar_borrar_animal.php"
                        method="post"
                        style="display:inline"
                        onsubmit="return confirm('Estás seguro de que queres borrar este animal?');"
                    >
                        <input
                            type="hidden"
                            name="nombre"
                            value="<?= htmlspecialchars($animal['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
                        >
                        <button type="submit" name="btnBorrarAnimal" 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>

Como empregamos un LEFT JOIN, fecha_inicio pode chegar como NULL. Comprobámolo antes de crear o obxecto DateTime.


Carga das pistas e atraccións dos formularios

Os formularios de alta e modificación necesitan todas as pistas e atraccións dispoñibles.



Arquivo animais/campos_animal.php

<label for="nombre">Nome:</label>
<input
    type="text"
    id="nombre"
    name="nombre"
    required
    value="<?= htmlspecialchars($nombre ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
>
<br>

<label for="tipo">Tipo:</label>
<select id="tipo" name="tipo" required>

    <?php
        $tipos = [
            'León',
            'Cocodrilo',
            'Jirafa',
            'Mono'
        ];
    ?>

    <?php foreach ($tipos as $tipoOption): ?>
        <option
            value="<?= htmlspecialchars($tipoOption, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
            <?= ($tipo ?? '') === $tipoOption ? 'selected' : '' ?>
        >
            <?= htmlspecialchars($tipoOption, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
        </option>
    <?php endforeach; ?>

</select>
<br>

<label for="anhos">Anos:</label>
<input
    type="number"
    id="anhos"
    name="anhos"
    min="1"
    max="50"
    value="<?= htmlspecialchars(isset($anhos) ? (string) $anhos : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
>
<br>

<label for="peso">Peso:</label>
<input
    type="number"
    id="peso"
    name="peso"
    min="0.1"
    max="500"
    step="any"
    value="<?= htmlspecialchars(isset($peso) ? (string) $peso : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
>
<br>

<label for="estatura">Estatura:</label>
<input
    type="number"
    id="estatura"
    name="estatura"
    min="0.1"
    max="20"
    step="any"
    value="<?= htmlspecialchars(isset($estatura) ? (string) $estatura : '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
>
<br>

<label for="nombre_atraccion">Nome atracción:</label>
<select id="nombre_atraccion" name="nombre_atraccion">
    <option value="">Sen asignar</option>

    <?php foreach ($atraccions as $atraccion): ?>
        <option
            value="<?= htmlspecialchars($atraccion['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
            <?= ($nombre_atraccion ?? null) === $atraccion['nombre'] ? 'selected' : '' ?>
        >
            <?= htmlspecialchars($atraccion['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
        </option>
    <?php endforeach; ?>

</select>
<br>

<label for="nombre_pista">Nome pista:</label>
<select id="nombre_pista" name="nombre_pista">
    <option value="">Sen asignar</option>

    <?php foreach ($pistas as $pista): ?>
        <option
            value="<?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
            <?= ($nombre_pista ?? null) === $pista['nombre'] ? 'selected' : '' ?>
        >
            <?= htmlspecialchars($pista['nombre'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
        </option>
    <?php endforeach; ?>

</select>

A comparación para decidir se unha opción está seleccionada faise cos valores orixinais. O escapado con htmlspecialchars() aplícase unicamente cando o valor se introduce no HTML. Por tanto, os acentos non producen problemas nas comparacións.


Arquivo animais/alta_animal.php

<?php

try {

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

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

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

    $stmt = $conex->query(CONSULTAR_ATRACCIONS);
    $atraccions = $stmt->fetchAll();

    $stmt = $conex->query(CONSULTAR_PISTAS);
    $pistas = $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>Alta de animal</title>
</head>
<body>

    <h1>Alta de animal</h1>

    <form method="post" action="procesar_alta_animal.php">
        <?php require 'campos_animal.php'; ?>
        <button type="submit" name="btnAltaAnimal" value="ALTA">Dar de alta</button>
    </form>

</body>
</html>


Validación dos datos opcionais

Se un campo opcional chega como cadea baleira, podemos convertelo en null:

$nombrePista = trim(
    $_POST['nombre_pista'] ?? ''
);

$nombrePista = $nombrePista === ''
    ? null
    : $nombrePista;

Para un número opcional é mellor distinguir explicitamente a cadea baleira dun valor numérico:

$anhosTexto = trim(
    $_POST['anhos'] ?? ''
);

if ($anhosTexto === '') {

    $anhos = null;

} else {

    $anhos = filter_var(
        $anhosTexto,
        FILTER_VALIDATE_INT,
        [
            'options' => [
                'min_range' => 1,
                'max_range' => 50
            ]
        ]
    );
}

Non empregaremos empty() como regra xeral para validar números porque tamén considera baleiros valores como 0 e '0'.


Arquivo animais/procesar_alta_animal.php

<?php

try {

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

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

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

    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnAltaAnimal']) || $_POST['btnAltaAnimal'] !== 'ALTA') {
        throw new RuntimeException('Operación non válida.');
    }

    $nombre = trim($_POST['nombre'] ?? '');
    $tipo = trim($_POST['tipo'] ?? '');

    $tiposPermitidos = ['León', 'Cocodrilo', 'Jirafa', 'Mono'];

    if ($nombre === '' || !in_array($tipo, $tiposPermitidos, true)) {
        header('Location: consultar_animais.php?error=' . urlencode('O nome ou o tipo non son válidos.'));
        exit;
    }

    $anhosTexto = trim($_POST['anhos'] ?? '');
    $anhos = null;

    if ($anhosTexto !== '') {
        $anhos = filter_var(
            $anhosTexto,
            FILTER_VALIDATE_INT,
            ['options' => ['min_range' => 1, 'max_range' => 50]]
        );

        if ($anhos === false) {
            header('Location: consultar_animais.php?error=' . urlencode('O número de anos non é válido.'));
            exit;
        }
    }

    $pesoTexto = trim($_POST['peso'] ?? '');
    $peso = null;

    if ($pesoTexto !== '') {
        $peso = filter_var($pesoTexto, FILTER_VALIDATE_FLOAT);

        if ($peso === false || $peso < 0.1 || $peso > 500) {
            header('Location: consultar_animais.php?error=' . urlencode('O peso non é válido.'));
            exit;
        }
    }

    $estaturaTexto = trim($_POST['estatura'] ?? '');
    $estatura = null;

    if ($estaturaTexto !== '') {
        $estatura = filter_var($estaturaTexto, FILTER_VALIDATE_FLOAT);

        if ($estatura === false || $estatura < 0.1 || $estatura > 20) {
            header('Location: consultar_animais.php?error=' . urlencode('A estatura non é válida.'));
            exit;
        }
    }

    $nombreAtraccion = trim($_POST['nombre_atraccion'] ?? '');
    $nombreAtraccion = $nombreAtraccion === '' ? null : $nombreAtraccion;

    $nombrePista = trim($_POST['nombre_pista'] ?? '');
    $nombrePista = $nombrePista === '' ? null : $nombrePista;

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

    $stmt->bindValue(':nombre', $nombre, PDO::PARAM_STR);
    $stmt->bindValue(':tipo', $tipo, PDO::PARAM_STR);

    $stmt->bindValue(
        ':anhos',
        $anhos,
        $anhos === null ? PDO::PARAM_NULL : PDO::PARAM_INT
    );

    $stmt->bindValue(
        ':peso',
        $peso === null ? null : (string) $peso,   // $peso es double, pero no existe PARAM_DOUBLE en PDO polo que temos que envialo como String
        $peso === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->bindValue(
        ':estatura',
        $estatura === null ? null : (string) $estatura,    // $estatura es double, pero no existe PARAM_DOUBLE en PDO polo que temos que envialo como String
        $estatura === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->bindValue(
        ':nombre_atraccion',
        $nombreAtraccion,
        $nombreAtraccion === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->bindValue(
        ':nombre_pista',
        $nombrePista,
        $nombrePista === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->execute();

    header('Location: consultar_animais.php?msg=' . urlencode('Animal dado de alta correctamente.'));
    exit;

} catch (PDOException $e) {

    error_log($e->getMessage());
    $codigoMySQL = $e->errorInfo[1] ?? null;

    if ($codigoMySQL === 1062) {
        $mensaxe = 'Xa existe un animal con ese nome.';
    } elseif ($codigoMySQL === 1452) {
        $mensaxe = 'A pista ou a atracción seleccionada non é válida.';
    } else {
        $mensaxe = 'Erro relacionado coa base de datos.';
    }

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

} catch (Throwable $e) {

    error_log($e->getMessage());
    header('Location: consultar_animais.php?error=' . urlencode('Produciuse un erro na aplicación.'));
    exit;
}

PDO non dispón dun tipo PDO::PARAM_FLOAT. Neste exemplo os valores decimais non nulos envíanse como cadeas, mentres que os valores ausentes se ligan explicitamente como PDO::PARAM_NULL.


Arquivo animais/editar_animal.php

<?php

try {

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

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

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

    $nombreRecibido = trim($_GET['nombre'] ?? '');

    if ($nombreRecibido === '') {
        header('Location: consultar_animais.php?error=' . urlencode('Non se indicou un animal válido.'));
        exit;
    }

    $stmt = $conex->prepare(CONSULTAR_ANIMAL_POR_NOME);
    $stmt->execute([':nombre' => $nombreRecibido]);
    $animal = $stmt->fetch();

    if ($animal === false) {
        header('Location: consultar_animais.php?error=' . urlencode('Non se atopou o animal indicado.'));
        exit;
    }

    $nombre = $animal['nombre'];
    $tipo = $animal['tipo'];
    $anhos = $animal['anhos'];
    $peso = $animal['peso'];
    $estatura = $animal['estatura'];
    $nombre_atraccion = $animal['nombre_atraccion'];
    $nombre_pista = $animal['nombre_pista'];

    $stmt = $conex->query(CONSULTAR_ATRACCIONS);
    $atraccions = $stmt->fetchAll();

    $stmt = $conex->query(CONSULTAR_PISTAS);
    $pistas = $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>Modificar animal</title>
</head>
<body>

    <h1>Modificar animal</h1>

    <form method="post" action="procesar_editar_animal.php">
        <?php require 'campos_animal.php'; ?>

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

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

</body>
</html>

Os valores recuperados da base de datos mantéñense sen codificar nas variables. O escapado faise unicamente no momento de xerar a saída HTML, polo que as comparacións dos select funcionan correctamente aínda que existan acentos.


Arquivo animais/procesar_editar_animal.php

O tratamento dos campos opcionais é idéntico ao realizado na alta.

<?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,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );

    if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['btnModificarAnimal']) || $_POST['btnModificarAnimal'] !== 'MODIFICAR') {
        throw new RuntimeException('Operación non válida.');
    }

    $nombreNovo = trim($_POST['nombre'] ?? '');
    $nombreOrixinal = trim($_POST['nome_orixinal'] ?? '');
    $tipo = trim($_POST['tipo'] ?? '');

    $tiposPermitidos = ['León', 'Cocodrilo', 'Jirafa', 'Mono'];

    if ($nombreNovo === '' || $nombreOrixinal === '' || !in_array($tipo, $tiposPermitidos, true)) {
        header('Location: consultar_animais.php?error=' . urlencode('O nome ou o tipo non son válidos.'));
        exit;
    }

    $anhosTexto = trim($_POST['anhos'] ?? '');
    $anhos = null;

    if ($anhosTexto !== '') {
        $anhos = filter_var(
            $anhosTexto,
            FILTER_VALIDATE_INT,
            ['options' => ['min_range' => 1, 'max_range' => 50]]
        );

        if ($anhos === false) {
            header('Location: consultar_animais.php?error=' . urlencode('O número de anos non é válido.'));
            exit;
        }
    }

    $pesoTexto = trim($_POST['peso'] ?? '');
    $peso = null;

    if ($pesoTexto !== '') {
        $peso = filter_var($pesoTexto, FILTER_VALIDATE_FLOAT);

        if ($peso === false || $peso < 0.1 || $peso > 500) {
            header('Location: consultar_animais.php?error=' . urlencode('O peso non é válido.'));
            exit;
        }
    }

    $estaturaTexto = trim($_POST['estatura'] ?? '');
    $estatura = null;

    if ($estaturaTexto !== '') {
        $estatura = filter_var($estaturaTexto, FILTER_VALIDATE_FLOAT);

        if ($estatura === false || $estatura < 0.1 || $estatura > 20) {
            header('Location: consultar_animais.php?error=' . urlencode('A estatura non é válida.'));
            exit;
        }
    }

    $nombreAtraccion = trim($_POST['nombre_atraccion'] ?? '');
    $nombreAtraccion = $nombreAtraccion === '' ? null : $nombreAtraccion;

    $nombrePista = trim($_POST['nombre_pista'] ?? '');
    $nombrePista = $nombrePista === '' ? null : $nombrePista;

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

    $stmt->bindValue(':nombre_novo', $nombreNovo, PDO::PARAM_STR);
    $stmt->bindValue(':tipo', $tipo, PDO::PARAM_STR);

    $stmt->bindValue(
        ':anhos',
        $anhos,
        $anhos === null ? PDO::PARAM_NULL : PDO::PARAM_INT
    );

    $stmt->bindValue(
        ':peso',
        $peso === null ? null : (string) $peso,
        $peso === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->bindValue(
        ':estatura',
        $estatura === null ? null : (string) $estatura,
        $estatura === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->bindValue(
        ':nombre_atraccion',
        $nombreAtraccion,
        $nombreAtraccion === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->bindValue(
        ':nombre_pista',
        $nombrePista,
        $nombrePista === null ? PDO::PARAM_NULL : PDO::PARAM_STR
    );

    $stmt->bindValue(':nombre_orixinal', $nombreOrixinal, PDO::PARAM_STR);

    $stmt->execute();

    $mensaxe = $stmt->rowCount() === 1
        ? 'Animal actualizado correctamente.'
        : 'Non se modificou ningunha fila (pode non existir ou non haber cambios).';

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

} catch (PDOException $e) {

    error_log($e->getMessage());
    $codigoMySQL = $e->errorInfo[1] ?? null;

    if ($codigoMySQL === 1062) {
        $mensaxe = 'Xa existe un animal co novo nome indicado.';
    } elseif ($codigoMySQL === 1452) {
        $mensaxe = 'A pista ou a atracción seleccionada non é válida.';
    } else {
        $mensaxe = 'Erro relacionado coa base de datos.';
    }

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

} catch (Throwable $e) {

    error_log($e->getMessage());
    header('Location: consultar_animais.php?error=' . urlencode('Produciuse un erro na aplicación.'));
    exit;
}


Arquivo animais/procesar_borrar_animal.php

<?php

try {

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

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

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

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

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

    if ($nombre === '') {
        header('Location: consultar_animais.php?error=' . urlencode('Non se indicou un animal válido.'));
        exit;
    }

    $stmt = $conex->prepare(BORRAR_ANIMAL);
    $stmt->execute([':nombre' => $nombre]);

    $mensaxe = $stmt->rowCount() === 1
        ? 'Animal borrado correctamente.'
        : 'Non se atopou o animal que se quería borrar.';

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

} catch (PDOException $e) {

    error_log($e->getMessage());
    $codigoMySQL = $e->errorInfo[1] ?? null;

    $mensaxe = $codigoMySQL === 1451
        ? 'O animal está relacionado con outros datos e non se pode borrar.'
        : 'Erro relacionado coa base de datos.';

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

} catch (Throwable $e) {

    error_log($e->getMessage());
    header('Location: consultar_animais.php?error=' . urlencode('Produciuse un erro na aplicación.'));
    exit;
}


mysqli como ampliación

PDO é o mecanismo principal empregado nestes apuntes. A extensión mysqli tamén permite realizar estas operacións correctamente, pero empregaremos igualmente consultas preparadas.

Conexión

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

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

INSERT preparado

$stmt = $conex->prepare(
    'INSERT INTO PISTAS (nombre, aforo)
     VALUES (?, ?)'
);

$stmt->bind_param(
    'si',
    $nombre,
    $aforo
);

$stmt->execute();

SELECT preparado

$stmt = $conex->prepare(
    'SELECT nombre, aforo
     FROM PISTAS
     WHERE nombre = ?'
);

$stmt->bind_param(
    's',
    $nombre
);

$stmt->execute();
$resultado = $stmt->get_result();
$pista = $resultado->fetch_assoc();

UPDATE preparado

$stmt = $conex->prepare(
    'UPDATE PISTAS
     SET nombre = ?,
         aforo = ?
     WHERE nombre = ?'
);

$stmt->bind_param(
    'sis',
    $nombreNovo,
    $aforo,
    $nombreOrixinal
);

$stmt->execute();

DELETE preparado

$stmt = $conex->prepare(
    'DELETE FROM PISTAS
     WHERE nombre = ?'
);

$stmt->bind_param(
    's',
    $nombre
);

$stmt->execute();

Non empregaremos como patrón principal real_escape_string() combinado con sprintf() para introducir valores nunha orde SQL.


Exercicios propostos

Se realizaches os exercicios anteriores xa tes unha estrutura que podes reutilizar para o mantemento doutras táboas.

Podes escoller outra táboa e implementar listado, alta, modificación, borrado, validación no servidor, consultas preparadas e tratamento de erros.


Exercicio con autores e libros

Dado o nome e apelidos dun autor seleccionado nun elemento <select>, amosar nunha táboa a lista de títulos escritos polo autor, ordenados polo título.

Se o autor non ten libros deberá aparecer:

Sen libros escritos

O identificador do autor debe enviarse mediante unha consulta preparada.


Listado de libros con paxinación

Amosar os libros con paxinación. Asociar a cada libro un elemento que permita seleccionalo e, ao realizar a selección, amosar os exemplares dese libro coa imaxe, prezo e estado.


Busca de libros

Dispoñer dunha caixa de texto na que o usuario escriba parte do título dun libro.

Realizar unha consulta con LIKE e amosar os libros que coincidan co patrón xunto co número de exemplares de cada libro.

O patrón constrúese como valor PHP:

$patron = '%' . $texto . '%';

$stmt = $conex->prepare(
    'SELECT ...
     WHERE titulo LIKE :patron'
);

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

Os caracteres % forman parte do valor do parámetro; a entrada do usuario non se concatena dentro da orde SQL.


Mantemento dunha relación N:M

Nunha relación N:M podemos seleccionar un elemento da primeira táboa, consultar todos os elementos da segunda e amosar un checkbox por cada un deles.

Por exemplo:

  • seleccionar unha atracción;
  • amosar todos os artistas;
  • marcar os artistas que traballan nela;
  • permitir engadir ou eliminar asociacións.

Como unha única acción do usuario pode provocar varios INSERT e DELETE, este tipo de operación é un bo exemplo para empregar unha transacción:

$conex->beginTransaction();

try {

    // DELETE das relacións eliminadas.
    // INSERT das novas relacións.

    $conex->commit();

} catch (Throwable $e) {

    if ($conex->inTransaction()) {
        $conex->rollBack();
    }

    throw $e;
}

Deste modo todas as modificacións se confirman xuntas ou se desfán se se produce un erro.


Resumo

  • Empregaremos PDO como mecanismo principal de acceso á base de datos.
  • O arquivo /var/www/confBD/bd.php queda fóra de /var/www/html.
  • As credenciais e as consultas estarán centralizadas nese arquivo.
  • As consultas con datos variables realizaranse con prepare().
  • Non introduciremos datos do usuario na SQL mediante sprintf(), concatenación ou interpolación.
  • Os datos validaranse sempre no servidor.
  • O escapado HTML realizarase no momento de xerar a saída mediante htmlspecialchars().
  • Non gardaremos entidades HTML na base de datos como mecanismo xeral de seguridade.
  • As operacións que modifican datos empregarán POST.
  • A protección CSRF incorporarase cando se estuden as sesións.
  • Os campos hidden poden ser manipulados e deben tratarse como datos non confiables.
  • As claves primarias modificables requiren conservar o valor orixinal para localizar a fila.
  • Os conflitos de clave duplicada deben estar garantidos pola base de datos e tratados pola aplicación.
  • Os valores opcionais representaranse en PHP mediante null.
  • Con PDO empregaremos PDO::PARAM_NULL cando necesitemos ligar explicitamente un valor SQL NULL.
  • Non construiremos manualmente a cadea 'null' nin faremos str_replace() sobre a consulta.
  • Os valores usados para decidir unha opción selected compararanse na súa forma orixinal.
  • htmlspecialchars() aplicarase só cando xeremos o atributo ou texto HTML correspondente.
  • Os erros técnicos rexistraranse con error_log().
  • Non mostraremos directamente $e->getMessage() ao usuario.
  • mysqli queda como ampliación, tamén empregando consultas preparadas.



Comprobación final dos exercicios

Antes de dar por rematada a práctica comprobaremos que:

  1. As listaxes funcionan sen parámetros e as consultas dunha fila empregan prepare() e execute().
  2. O alta valida os campos no servidor, insire mediante POST e trata as claves duplicadas.
  3. A modificación conserva a clave primaria orixinal e permite cambiar o nome da pista ou do animal.
  4. O borrado só se realiza mediante POST e trata posibles conflitos de integridade referencial.
  5. Os campos opcionais baleiros gárdanse como NULL cando corresponde e non como texto 'null'.
  6. As etiquetas, textos e atributos HTML amosan os datos mediante htmlspecialchars().
  7. Non se mostran credenciais nin mensaxes técnicas de excepción no navegador.
  8. As probas inclúen entradas mal formadas, valores límite e rexistros inexistentes.




Ampliación dos exercicios

  • Realizar unha segunda versión dos exercicios anteriores nas páxinas de 'procesar....' para que se chame a un procedemento almacenado de Mysql.
Define a chamada ao procedemento o os parámetros necesarios no arquivo bd.php.