Diferencia entre revisiones de «PHP UD3 EXERCICIOS PROPOSTOS»
| (No se muestran 12 ediciones intermedias del mismo usuario) | |||
| Línea 1: | Línea 1: | ||
| − | == | + | == 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: | ||
| − | * ''' | + | :* '''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 /> | ||
| + | |||
| + | * '''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 /> | <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]] | ||
| + | |||
| + | :* 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 /> | <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 /> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | == 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 /> | ||
| − | == | + | === Esquema común para acceder á base de datos === |
| − | + | Nas páxinas PHP que accedan á base de datos empregaremos o seguinte esquema: | |
| − | ::<syntaxhighlight lang="php" | + | |
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
<?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 | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | // Operacións coa base de datos... | ||
| + | |||
| + | } catch (PDOException $e) { | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | die('Erro relacionado coa base de datos.'); | ||
| + | |||
| + | } catch (Throwable $e) { | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | die('Produciuse un erro na aplicación.'); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === 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 /> | ||
| + | === Saída HTML === | ||
| + | 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> | </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 /> | <br /> | ||
| − | ==Exercicio 1== | + | == 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 79: | 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 /> | ||
| − | === | + | |
| − | + | === Arquivo pistas/consultar_pistas.php === | |
| − | ::<syntaxhighlight lang="php" enclose="div | + | |
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
<?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> | <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> | </tr> | ||
| − | |||
| − | |||
| − | + | <?php endforeach; ?> | |
| − | + | ||
| − | + | </tbody> | |
| − | + | </table> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <?php if (isset($_GET['msg'])): ?> | |
| − | + | <p><?= htmlspecialchars($_GET['msg'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p> | |
| − | + | <?php endif; ?> | |
| + | <?php if (isset($_GET['error'])): ?> | ||
| + | <p><?= htmlspecialchars($_GET['error'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p> | ||
| + | <?php endif; ?> | ||
| − | + | </body> | |
| − | |||
</html> | </html> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
</syntaxhighlight> | </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> | <label for="aforo">Aforo:</label> | ||
| − | <input type="number" | + | <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"> | |
| − | |||
| − | ::<syntaxhighlight lang="php" | ||
<!DOCTYPE html> | <!DOCTYPE html> | ||
| − | <html> | + | <html lang="gl"> |
<head> | <head> | ||
| − | + | <meta charset="UTF-8"> | |
| + | <title>Alta de pista</title> | ||
</head> | </head> | ||
<body> | <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> | </body> | ||
</html> | </html> | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Arquivo pistas/procesar_alta_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['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> | </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"> | |
| − | |||
| − | ::<syntaxhighlight lang="php" | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | require | + | 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> | <!DOCTYPE html> | ||
| − | <html> | + | <html lang="gl"> |
<head> | <head> | ||
| − | + | <meta charset="UTF-8"> | |
| + | <title>Modificar pista</title> | ||
</head> | </head> | ||
| − | <body> | + | <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> | </body> | ||
</html> | </html> | ||
| − | |||
</syntaxhighlight> | </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"> | |
| − | |||
| − | ::<syntaxhighlight lang="php" | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | require | + | try { |
| − | $conex = new | + | |
| − | + | 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 (isset($_POST['btnModificarPista'])) { | + | 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, | |
| − | if( | + | 'max_range' => 500 |
| − | + | ] | |
| − | + | ] | |
| − | + | ); | |
| − | + | ||
| − | header( | + | 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; | ||
| + | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | <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; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
<br /> | <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><select></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"> | <gallery caption="Xestión da táboa ANIMALES" widths="350" heights="300px" perrow="2"> | ||
| Línea 469: | Línea 822: | ||
Image:UD3_op_bd_ex_2_B.jpg| Formulario de alta de animal. | 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_C.jpg| Formulario de modificación de animal. | ||
| − | Image:UD3_op_bd_ex_2_D.jpg| Confirmación borrado de animal. | + | Image:UD3_op_bd_ex_2_D.jpg| Confirmación de borrado de animal. |
</gallery> | </gallery> | ||
| + | <br /> | ||
| + | === Valores NULL con PDO === | ||
| − | < | + | 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> | ||
| − | + | 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', | |
| − | ::<syntaxhighlight lang="php" enclose="div | + | 'SELECT nombre, fecha_inicio, ganancias |
| − | + | FROM ATRACCIONES | |
| − | + | ORDER BY nombre' | |
| + | ); | ||
| − | define(' | + | 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(' | + | 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> | </syntaxhighlight> | ||
| + | Reutilizaremos tamén <code>CONSULTAR_PISTAS</code>, definida no exercicio anterior. | ||
| + | |||
| + | <br /> | ||
| + | === 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); | |
| − | $conex | + | $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 lang="gl"> | |
| − | <html | + | <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> | <tr> | ||
<th>Nome</th> | <th>Nome</th> | ||
| Línea 559: | Línea 993: | ||
<th>Peso</th> | <th>Peso</th> | ||
<th>Estatura</th> | <th>Estatura</th> | ||
| − | <th> | + | <th>Atracción</th> |
| − | <th> | + | <th>Data inicio</th> |
| − | <th> | + | <th>Pista</th> |
<th>Aforo</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> | </tr> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <?php endforeach; ?> | |
| − | + | </tbody> | |
| − | + | </table> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <?php if (isset($_GET['msg'])): ?> | |
| − | + | <p><?= htmlspecialchars($_GET['msg'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p> | |
| − | + | <?php endif; ?> | |
| + | <?php if (isset($_GET['error'])): ?> | ||
| + | <p><?= htmlspecialchars($_GET['error'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p> | ||
| + | <?php endif; ?> | ||
| − | + | </body> | |
| − | |||
</html> | </html> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
</syntaxhighlight> | </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 /> | <br /> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | ::<syntaxhighlight lang="php" | + | === 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 === | ||
| + | ::<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_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> | ||
| − | + | <meta charset="UTF-8"> | |
| + | <title>Alta de animal</title> | ||
</head> | </head> | ||
<body> | <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> | </body> | ||
</html> | </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 /> | ||
| − | + | ||
| − | ::<syntaxhighlight lang="php" | + | === Arquivo animais/procesar_alta_animal.php === |
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | require | + | try { |
| − | if (isset($_POST['btnAltaAnimal'])) { | + | |
| − | + | require '/var/www/confBD/bd.php'; | |
| − | $conex = | + | |
| − | $ | + | $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) { | |
| − | if | + | $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> | </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"> | |
| − | |||
| − | ::<syntaxhighlight lang="php" | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | require | + | 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> | <!DOCTYPE html> | ||
| − | <html> | + | <html lang="gl"> |
<head> | <head> | ||
| − | + | <meta charset="UTF-8"> | |
| + | <title>Modificar animal</title> | ||
</head> | </head> | ||
| − | <body> | + | <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> | </body> | ||
</html> | </html> | ||
</syntaxhighlight> | </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> | ||
<br /> | <br /> | ||
| − | + | ||
| − | ::<syntaxhighlight lang="php" | + | === Arquivo animais/procesar_borrar_animal.php === |
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | try { | |
| − | + | ||
| − | require | + | require '/var/www/confBD/bd.php'; |
| − | $conex = new | + | |
| − | $conex-> | + | $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 /> | |
| − | </ | ||
| + | == 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"> | |
| − | |||
| − | ::<syntaxhighlight lang="php" | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | require | + | try { |
| − | + | ||
| − | + | require '/var/www/confBD/bd.php'; | |
| − | $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD); | + | |
| + | mysqli_report( | ||
| + | MYSQLI_REPORT_ERROR | | ||
| + | MYSQLI_REPORT_STRICT | ||
| + | ); | ||
| + | |||
| + | $conex = new mysqli( | ||
| + | SERVIDOR, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | BD | ||
| + | ); | ||
| + | |||
$conex->set_charset('utf8mb4'); | $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.'); | |
| − | |||
| − | |||
| − | die( | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | + | </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><select></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 /> | ||
| + | |||
| + | === Busca de libros === | ||
| + | |||
| + | Dispoñer dunha caixa de texto na que o usuario escriba parte do título dun libro. | ||
| + | |||
| + | 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. | ||
| + | |||
| + | O patrón constrúese como valor PHP: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $patron = '%' . $texto . '%'; | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT ... | ||
| + | WHERE titulo LIKE :patron' | ||
| + | ); | ||
| + | $stmt->execute([ | ||
| + | ':patron' => $patron | ||
| + | ]); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Os caracteres <code>%</code> forman parte do valor do parámetro; a entrada do usuario non se concatena dentro da orde SQL. | ||
<br /> | <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. | ||
| + | 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> | ||
| + | |||
| + | Deste modo todas as modificacións se confirman xuntas ou se desfán se se produce un erro. | ||
<br /> | <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> | + | |
| + | |||
| + | <br /> | ||
| + | == 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. | ||
Revisión actual del 10:33 8 oct 2026
Sumario
- 1 Introdución
- 2 Para todos os exercicios
- 3 Exercicio 1: mantemento da táboa PISTAS
- 3.1 Consultas necesarias
- 3.2 Arquivo pistas/consultar_pistas.php
- 3.3 Arquivo pistas/campos_pista.php
- 3.4 Arquivo pistas/alta_pista.php
- 3.5 Arquivo pistas/procesar_alta_pista.php
- 3.6 Arquivo pistas/editar_pista.php
- 3.7 Arquivo pistas/procesar_editar_pista.php
- 3.8 Arquivo pistas/procesar_borrar_pista.php
- 4 Exercicio 2: mantemento da táboa ANIMALES
- 4.1 Valores NULL con PDO
- 4.2 Consultas necesarias
- 4.3 Arquivo animais/consultar_animais.php
- 4.4 Carga das pistas e atraccións dos formularios
- 4.5 Arquivo animais/campos_animal.php
- 4.6 Arquivo animais/alta_animal.php
- 4.7 Validación dos datos opcionais
- 4.8 Arquivo animais/procesar_alta_animal.php
- 4.9 Arquivo animais/editar_animal.php
- 4.10 Arquivo animais/procesar_editar_animal.php
- 4.11 Arquivo animais/procesar_borrar_animal.php
- 5 mysqli como ampliación
- 6 Exercicios propostos
- 7 Resumo
- 8 Comprobación final dos exercicios
- 9 Ampliación dos exercicios
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.
- Create →
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
POSTpara 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.
- Ficheiro SQL: Archivo: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:
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,UPDATEeDELETE. O permisoEXECUTEsó 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
CIRCOen MySQL 8 e comprobar a estrutura e as restricións das táboas. - Crear un usuario específico con permisos mínimos (
SELECT,INSERT,UPDATEeDELETE) sobre as táboas necesarias. Non empregarrootna 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_errorsnon 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,UPDATEeDELETE. - As consultas preparadas non deben reconstruírse concatenando datos do formulario; os valores variables enlázanse con parámetros.
- Importar a base de datos
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
- Xestión da táboa PISTAS
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
PISTASeATRACCIONES; - 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.
- relacións con
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
- Xestión da táboa ANIMALES
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.phpqueda 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
hiddenpoden 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_NULLcando necesitemos ligar explicitamente un valor SQLNULL. - Non construiremos manualmente a cadea
'null'nin faremosstr_replace()sobre a consulta. - Os valores usados para decidir unha opción
selectedcompararanse 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. mysqliqueda 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
prepare()eexecute(). - O alta valida os campos no servidor, insire mediante
POSTe 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
POSTe trata posibles conflitos de integridade referencial. - Os campos opcionais baleiros gárdanse como
NULLcando corresponde e non como texto'null'. - As etiquetas, textos e atributos HTML amosan os datos mediante
htmlspecialchars(). - 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.
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.