PHP UD3 EXERCICIOS PROPOSTOS

De MediaWiki
Ir a la navegación Ir a la búsqueda

Introdución

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

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

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

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

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


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


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


Consideracións sobre os exercicios

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

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

PISTAS.nombre
ANIMALES.nombre

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

Por tanto teremos que conservar:

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

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


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

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


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

Empregaremos:

/var/www/confBD/bd.php


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


Para todos os exercicios

Arquivo /var/www/confBD/bd.php

O arquivo terá inicialmente:

<?php

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

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


Esquema común para acceder á base de datos

Nas páxinas PHP que accedan á base de datos empregaremos o seguinte esquema:

<?php

try {

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

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

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

    // Operacións coa base de datos...

} catch (PDOException $e) {

    error_log($e->getMessage());

    die('Erro relacionado coa base de datos.');

} catch (Throwable $e) {

    error_log($e->getMessage());

    die('Produciuse un erro na aplicación.');
}


Consultas con e sen parámetros

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

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

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

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

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

$pista = $stmt->fetch();

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


Saída HTML

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

Aplicaremos htmlspecialchars() cando os introduzamos nun documento HTML:

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

Para un valor que pode ser NULL podemos facer:

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

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


Exercicio 1: mantemento da táboa PISTAS

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

Crearemos o cartafol:

pistas/

coas seguintes páxinas:

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


Consultas necesarias

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

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

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

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

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

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


Arquivo pistas/consultar_pistas.php

<?php

try {

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

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

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

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

} catch (PDOException $e) {
    error_log($e->getMessage());
    die('Erro relacionado coa base de datos.');
} catch (Throwable $e) {
    error_log($e->getMessage());
    die('Produciuse un erro na aplicación.');
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Xestión de pistas</title>
</head>
<body>

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

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

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

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

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

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

        <?php endforeach; ?>

        </tbody>
    </table>

    <?php if (isset($_GET['msg'])): ?>
        <p><?= htmlspecialchars($_GET['msg'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
    <?php endif; ?>

    <?php if (isset($_GET['error'])): ?>
        <p><?= htmlspecialchars($_GET['error'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
    <?php endif; ?>

</body>
</html>


Arquivo pistas/campos_pista.php

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

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

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


Arquivo pistas/alta_pista.php

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

    <h1>Alta de pista</h1>

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

</body>
</html>


Arquivo pistas/procesar_alta_pista.php

<?php

try {

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

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

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

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

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

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

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

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

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

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

} catch (PDOException $e) {

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

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

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

} catch (Throwable $e) {

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


Arquivo pistas/editar_pista.php

Esta páxina recibe o nome da pista mediante GET. É correcto porque a petición unicamente consulta os datos e amosa o formulario.

<?php

try {

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

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

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

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

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

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

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

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

} catch (PDOException $e) {
    error_log($e->getMessage());
    die('Erro relacionado coa base de datos.');
} catch (Throwable $e) {
    error_log($e->getMessage());
    die('Produciuse un erro na aplicación.');
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Modificar pista</title>
</head>
<body>

    <h1>Modificar pista</h1>

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

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

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

</body>
</html>

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


Arquivo pistas/procesar_editar_pista.php

<?php

try {

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

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

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

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

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

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

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

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

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

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

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

} catch (PDOException $e) {

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

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

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

} catch (Throwable $e) {

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


Arquivo pistas/procesar_borrar_pista.php

<?php

try {

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

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

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

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

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

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

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

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

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

} catch (PDOException $e) {

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

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

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

} catch (Throwable $e) {

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


Exercicio 2: mantemento da táboa ANIMALES

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

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

Os campos empregados no exercicio son:

nombre
tipo
anhos
peso
estatura
nombre_atraccion
nombre_pista

Os campos opcionais son:

anhos
peso
estatura
nombre_atraccion
nombre_pista

Crearemos o cartafol:

animais/

coas seguintes páxinas:

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


Valores NULL con PDO

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

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

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


Consultas necesarias

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

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

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

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

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

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

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

Reutilizaremos tamén CONSULTAR_PISTAS, definida no exercicio anterior.


Arquivo animais/consultar_animais.php

<?php

try {

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

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

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

    $stmt = $conex->query(CONSULTAR_ANIMAIS);
    $animais = $stmt->fetchAll();

} catch (PDOException $e) {
    error_log($e->getMessage());
    die('Erro relacionado coa base de datos.');
} catch (Throwable $e) {
    error_log($e->getMessage());
    die('Produciuse un erro na aplicación.');
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Xestión de animais</title>
</head>
<body>

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

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

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

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

            <?php
                $fechaInicio = '';

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

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

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

        <?php endforeach; ?>

        </tbody>
    </table>

    <?php if (isset($_GET['msg'])): ?>
        <p><?= htmlspecialchars($_GET['msg'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
    <?php endif; ?>

    <?php if (isset($_GET['error'])): ?>
        <p><?= htmlspecialchars($_GET['error'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
    <?php endif; ?>

</body>
</html>

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


Carga das pistas e atraccións dos formularios

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



Arquivo animais/campos_animal.php

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

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

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

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

</select>
<br>

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

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

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

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

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

</select>
<br>

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

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

</select>

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


Arquivo animais/alta_animal.php

<?php

try {

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

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

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

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

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

} catch (PDOException $e) {
    error_log($e->getMessage());
    die('Erro relacionado coa base de datos.');
} catch (Throwable $e) {
    error_log($e->getMessage());
    die('Produciuse un erro na aplicación.');
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Alta de animal</title>
</head>
<body>

    <h1>Alta de animal</h1>

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

</body>
</html>


Validación dos datos opcionais

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

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

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

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

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

if ($anhosTexto === '') {

    $anhos = null;

} else {

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

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


Arquivo animais/procesar_alta_animal.php

<?php

try {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    $stmt->execute();

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

} catch (PDOException $e) {

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

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

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

} catch (Throwable $e) {

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

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


Arquivo animais/editar_animal.php

<?php

try {

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

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

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

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

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

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

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

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

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

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

} catch (PDOException $e) {
    error_log($e->getMessage());
    die('Erro relacionado coa base de datos.');
} catch (Throwable $e) {
    error_log($e->getMessage());
    die('Produciuse un erro na aplicación.');
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Modificar animal</title>
</head>
<body>

    <h1>Modificar animal</h1>

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

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

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

</body>
</html>

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


Arquivo animais/procesar_editar_animal.php

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

<?php

try {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    $stmt->execute();

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

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

} catch (PDOException $e) {

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

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

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

} catch (Throwable $e) {

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


Arquivo animais/procesar_borrar_animal.php

<?php

try {

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

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

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

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

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

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

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

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

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

} catch (PDOException $e) {

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

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

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

} catch (Throwable $e) {

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


mysqli como ampliación

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

Conexión

<?php

try {

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

    mysqli_report(
        MYSQLI_REPORT_ERROR |
        MYSQLI_REPORT_STRICT
    );

    $conex = new mysqli(
        SERVIDOR,
        USUARIO,
        PASSWORD,
        BD
    );

    $conex->set_charset('utf8mb4');

    // Operacións...

} catch (mysqli_sql_exception $e) {

    error_log($e->getMessage());
    die('Erro relacionado coa base de datos.');

} catch (Throwable $e) {

    error_log($e->getMessage());
    die('Produciuse un erro na aplicación.');
}

INSERT preparado

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

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

$stmt->execute();

SELECT preparado

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

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

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

UPDATE preparado

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

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

$stmt->execute();

DELETE preparado

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

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

$stmt->execute();

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


Exercicios propostos

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

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


Exercicio con autores e libros

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

Se o autor non ten libros deberá aparecer:

Sen libros escritos

O identificador do autor debe enviarse mediante unha consulta preparada.


Listado de libros con paxinación

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


Busca de libros

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

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

O patrón constrúese como valor PHP:

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

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

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

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


Mantemento dunha relación N:M

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

Por exemplo:

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

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

$conex->beginTransaction();

try {

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

    $conex->commit();

} catch (Throwable $e) {

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

    throw $e;
}

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


Resumo

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



Comprobación final dos exercicios

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

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




Ampliación dos exercicios

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