Diferencia entre revisiones de «PHP UD3 OP ALTA»
| (No se muestran 28 ediciones intermedias del mismo usuario) | |||
| Línea 1: | Línea 1: | ||
| − | === | + | === Introdución === |
| + | Nesta unidade veremos como realizar operacións de alta sobre unha base de datos MySQL/MariaDB desde PHP 8.5. | ||
| − | + | O mecanismo principal empregado nos apuntes será '''PDO (PHP Data Objects)'''. PDO proporciona unha interface uniforme para traballar con diferentes sistemas xestores de bases de datos e permite empregar consultas preparadas con parámetros. | |
| − | |||
| − | |||
| − | |||
| + | Ao final da unidade inclúese tamén unha explicación equivalente empregando '''mysqli''', como ampliación e para poder interpretar código existente escrito con esta extensión. | ||
| + | * '''<u>Requirimentos previos:</u>''' | ||
| + | :* Ter creada a base de datos indicada [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql nunha sección anterior]. | ||
| + | :* Ter gardados os parámetros de conexión nun arquivo separado, fóra de <code>/var/www/html</code>. | ||
| + | :* Ter instalada e habilitada a extensión <code>pdo_mysql</code>. | ||
| + | :* Os exemplos están pensados para '''PHP 8.5'''. | ||
| + | |||
| + | |||
| + | |||
| + | * '''CONSIDERACIÓNS:''' | ||
| + | :* Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos) | ||
| + | :* Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro vimos nun punto anterior desta sección]. | ||
| + | :* Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas. | ||
| + | :* En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes. | ||
| + | :* Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...). | ||
| + | :* Lembrar que tamén podedes activar o debugger como está indicado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Habilitar_depuraci%C3%B3n_en_VS_Code nesta sección da wiki]. | ||
| − | |||
<br /> | <br /> | ||
| − | |||
| − | |||
| − | |||
| − | + | ==== Organización dos exemplos ==== | |
| + | Nos exemplos completos empregaremos o seguinte esquema. | ||
| + | |||
| + | No arquivo '''/var/www/confBD/bd.php''', que se atopa fóra da ruta pública <code>/var/www/html</code>, gardaremos os datos da conexión e as consultas que se vaian empregar: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | <?php | ||
| + | |||
| + | define('SERVIDOR', 'localhost'); | ||
| + | define('USUARIO', 'user_php'); | ||
| + | define('PASSWORD', 'user_php'); | ||
| + | define('BD', 'php'); | ||
| + | |||
| + | define( | ||
| + | 'NOME_DA_CONSULTA', | ||
| + | 'SELECT col1, col2 | ||
| + | FROM taboa | ||
| + | ORDER BY col1' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | Nas páxinas PHP que accedan á base de datos empregaremos como base o seguinte esquema: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | <?php | |
| + | try { | ||
| − | + | require '/var/www/confBD/bd.php'; | |
| − | + | $dsn = sprintf( | |
| − | : | + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', |
| − | + | SERVIDOR, | |
| − | + | BD | |
| − | + | ); | |
| + | $conex = new PDO( | ||
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| − | + | // Operacións coa base de datos... | |
| − | |||
| − | + | } catch (PDOException $e) { | |
| − | |||
| + | // Rexistrar o erro nun log. | ||
| + | // Nunha aplicación real tamén poderían empregarse | ||
| + | // ferramentas como Monolog. | ||
| + | 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> | ||
| + | '''IMPORTANTE:''' | ||
| + | * O usuario da aplicación non debe recibir detalles internos da excepción, nomes de táboas, rutas do servidor nin credenciais. | ||
| + | * Durante o desenvolvemento podemos consultar os logs do servidor para localizar o problema. | ||
| + | * En PHP 8 PDO emprega excepcións por defecto, pero nos exemplos indicamos explicitamente <code>PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION</code> para que o comportamento quede claro. | ||
| + | * Indicamos tamén <code>PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC</code>, polo que os métodos <code>fetch()</code> e <code>fetchAll()</code> devolverán por defecto arrays asociativos. | ||
<br /> | <br /> | ||
| − | + | ||
| − | :::<syntaxhighlight lang=" | + | ==== Consultas preparadas ==== |
| − | + | ||
| − | $ | + | Sempre que unha consulta conteña datos variables procedentes do usuario ou da aplicación, empregaremos unha '''consulta preparada'''. |
| − | + | ||
| − | + | Por exemplo, non construiremos unha consulta desta forma: | |
| − | + | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | $ | + | $consulta = sprintf( |
| + | "INSERT INTO estados (descripcion) VALUES ('%s')", | ||
| + | $descripcion | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Empregaremos un parámetro: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'INSERT INTO estados (descripcion) VALUES (:descripcion)' | ||
| + | ); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':descripcion' => $descripcion | ||
| + | ]); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | Os parámetros representan '''valores'''. Non se poden utilizar para substituír nomes de táboas, nomes de columnas, palabras clave SQL ou fragmentos arbitrarios dunha consulta. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Alta de rexistros: INSERT === | ||
| + | |||
| + | Para dar de alta novos rexistros nunha base de datos empregamos a instrución SQL <code>INSERT</code>. | ||
| + | |||
| + | Por exemplo: | ||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
| + | INSERT INTO TABOA_1 (col1, col2) | ||
| + | VALUES (123, 'Texto de proba'); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Desde PHP empregaremos normalmente unha consulta preparada. | ||
<br /> | <br /> | ||
| − | |||
| − | + | ==== Alta empregando PDO ==== | |
| − | : | + | |
| + | Supoñamos a seguinte táboa: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
| + | CREATE TABLE TABOA_1 ( | ||
| + | col1 INT PRIMARY KEY AUTO_INCREMENT, | ||
| + | col2 VARCHAR(100) NOT NULL | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Neste caso non indicaremos <code>col1</code> no <code>INSERT</code>, xa que MySQL/MariaDB xerará automaticamente o seu valor. | ||
| + | |||
| + | '''Arquivo /var/www/confBD/bd.php''' | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | <?php | ||
| + | |||
| + | define('SERVIDOR', 'localhost'); | ||
| + | define('USUARIO', 'user_php'); | ||
| + | define('PASSWORD', 'user_php'); | ||
| + | define('BD', 'php'); | ||
| + | |||
| + | define( | ||
| + | 'INSERIR_TABOA_1', | ||
| + | 'INSERT INTO TABOA_1 (col2) | ||
| + | VALUES (:col2)' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | '''Arquivo inserir_taboa_1.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 | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | $valor = 'Texto de proba'; | ||
| + | |||
| + | $stmt = $conex->prepare(INSERIR_TABOA_1); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':col2' => $valor | ||
| + | ]); | ||
| + | |||
| + | echo 'Rexistro dado de alta correctamente.'; | ||
| + | |||
| + | } catch (PDOException $e) { | ||
| − | + | error_log($e->getMessage()); | |
| − | + | die('Erro relacionado coa base de datos.'); | |
| − | + | } catch (Throwable $e) { | |
| − | + | error_log($e->getMessage()); | |
| − | |||
| − | + | die('Produciuse un erro na aplicación.'); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | </syntaxhighlight> | ||
| + | Neste exemplo: | ||
| + | |||
| + | :* <code>prepare()</code> prepara a orde SQL. | ||
| + | :* <code>:col2</code> é un parámetro con nome. | ||
| + | :* <code>execute()</code> envía o valor asociado ao parámetro e executa a orde. | ||
| + | :* Non é necesario engadir comiñas manualmente ao valor nin empregar <code>real_escape_string()</code>. | ||
<br /> | <br /> | ||
| − | |||
| − | + | ==== Número de filas afectadas ==== | |
| + | |||
| + | Despois dun <code>INSERT</code>, podemos consultar o número de filas afectadas mediante <code>rowCount()</code> sobre o obxecto <code>PDOStatement</code>: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $stmt->execute([ | ||
| + | ':col2' => $valor | ||
| + | ]); | ||
| + | $numRexistros = $stmt->rowCount(); | ||
| − | + | if ($numRexistros === 1) { | |
| − | + | echo 'Rexistro dado de alta correctamente.'; | |
| − | + | } | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | Nun <code>INSERT</code> dunha única fila o normal é obter o valor <code>1</code> se a operación se realizou. | ||
| + | |||
| + | '''Nota:''' non debemos utilizar <code>rowCount()</code> para determinar de forma portable o número de filas devoltas por un <code>SELECT</code>. Para un <code>SELECT</code> recuperaremos os datos con <code>fetch()</code>, <code>fetchColumn</code> ou <code>fetchAll()</code>. | ||
| + | |||
| + | <br /> | ||
| − | + | === Obtendo o valor dun campo autonumérico === | |
| + | Cando unha táboa ten unha clave primaria <code>AUTO_INCREMENT</code>, MySQL/MariaDB xera automaticamente o identificador da nova fila. | ||
| + | Non debemos inserir unha fila e realizar despois: | ||
| − | + | ::<syntaxhighlight lang="sql" enclose="div"> | |
| + | SELECT MAX(id) FROM taboa; | ||
| + | </syntaxhighlight> | ||
| − | + | Nunha aplicación con varios usuarios outra conexión podería inserir unha fila entre ambas as operacións. | |
| − | |||
| + | Con PDO empregaremos: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| + | $id = $conex->lastInsertId(); | ||
| + | </syntaxhighlight> | ||
| − | + | <code>lastInsertId()</code> devolve o identificador asociado á última inserción realizada pola conexión. | |
| − | |||
| − | : | + | Exemplo: |
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $stmt = $conex->prepare(INSERIR_TABOA_1); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':col2' => $valor | ||
| + | ]); | ||
| + | |||
| + | $id = $conex->lastInsertId(); | ||
| + | |||
| + | echo 'Novo rexistro co id ' . | ||
| + | htmlspecialchars($id, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); | ||
| + | </syntaxhighlight> | ||
| + | '''Nota:''' <code>PDO::lastInsertId()</code> devolve normalmente unha cadea. Se necesitamos tratar o identificador como enteiro podemos convertelo explicitamente: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $id = (int) $conex->lastInsertId(); | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | |||
| − | + | === Caso práctico: mantemento da táboa ESTADOS === | |
| − | + | Imos facer unha pequena parte do mantemento da táboa <code>ESTADOS</code>. | |
| − | |||
| − | |||
| − | |||
| + | Supoñamos: | ||
| − | + | ::<syntaxhighlight lang="sql" enclose="div"> | |
| − | + | CREATE TABLE estados ( | |
| + | id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, | ||
| + | descripcion VARCHAR(100) NOT NULL | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| − | + | Crearemos: | |
| + | :* <code>consultar_estados.php</code>: amosa os estados existentes. | ||
| + | :* <code>alta_estado.php</code>: amosa o formulario de alta. | ||
| + | :* <code>campos_estado.php</code>: contén os campos reutilizables do formulario. Isto quere decir que os campos van ser os mesmos se damos de alta ou se modificamos un rexistro, polo que nas dúas operacións (alta / modificar) podemos empregar o mesmo include cós campos tendo en conta que no caso de modificación teremos que ter o código php necesario para cargar o dato do rexistro a modificar. | ||
| + | :* <code>procesar_alta_estado.php</code>: valida os datos e realiza o <code>INSERT</code>. | ||
| + | :* <code>/var/www/confBD/bd.php</code>: contén credenciais e consultas SQL. | ||
| − | + | Neste punto do curso enviaremos o resultado da operación mediante parámetros na URL. Máis adiante, cando se estuden as sesións, será preferible empregar unha mensaxe gardada na sesión e facer o patrón '''POST/Redirect/GET'''. | |
| + | <br /> | ||
| − | + | ==== Arquivo /var/www/confBD/bd.php ==== | |
| − | ===== | + | |
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | <?php | ||
| + | |||
| + | define('SERVIDOR', 'localhost'); | ||
| + | define('USUARIO', 'user_php'); | ||
| + | define('PASSWORD', 'user_php'); | ||
| + | define('BD', 'php'); | ||
| + | |||
| + | define( | ||
| + | 'CONSULTAR_ESTADOS', | ||
| + | 'SELECT id, descripcion | ||
| + | FROM estados | ||
| + | ORDER BY descripcion' | ||
| + | ); | ||
| + | |||
| + | define( | ||
| + | 'CONSULTAR_ESTADO_NUM', | ||
| + | 'SELECT COUNT(*) AS existe | ||
| + | FROM estados | ||
| + | WHERE UPPER(descripcion) = UPPER(:descripcion)' | ||
| + | ); | ||
| − | + | define( | |
| − | + | 'INSERIR_ESTADO', | |
| − | + | 'INSERT INTO estados (descripcion) | |
| − | define ( | + | VALUES (:descripcion)' |
| − | + | ); | |
| − | |||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | Fixarse en que: | ||
| + | |||
| + | :* As credenciais non están nun arquivo accesible directamente desde a web. | ||
| + | :* As consultas SQL están centralizadas. | ||
| + | :* Os datos variables empréganse mediante parámetros como <code>:descripcion</code>. | ||
| + | :* Non se introduce directamente ningún dato do formulario dentro da cadea SQL. | ||
| + | <br /> | ||
| − | + | ==== Arquivo campos_estado.php ==== | |
| − | ::<syntaxhighlight lang=" | + | |
| − | < | + | ::<syntaxhighlight lang="html" enclose="div"> |
| − | + | <label for="descripcion">Descrición:</label> | |
| − | + | ||
| − | + | <input | |
| − | + | type="text" | |
| + | id="descripcion" | ||
| + | name="descripcion" | ||
| + | minlength="4" | ||
| + | maxlength="100" | ||
| + | required | ||
| + | > | ||
| + | </syntaxhighlight> | ||
| − | + | A validación HTML mellora a experiencia do usuario, pero '''non substitúe a validación no servidor'''. Unha petición HTTP pode enviarse sen utilizar o formulario. | |
| − | + | <br /> | |
| − | |||
| − | |||
| − | + | ==== Arquivo alta_estado.php ==== | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| + | <!DOCTYPE html> | ||
| + | <html lang="gl"> | ||
| + | <head> | ||
| + | <meta charset="UTF-8"> | ||
| + | <title>Alta de estado</title> | ||
| + | </head> | ||
| + | <body> | ||
| − | + | <h1>Alta de estado</h1> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <form method="post" action="procesar_alta_estado.php"> | |
| − | |||
| − | |||
| + | <?php require 'campos_estado.php'; ?> | ||
<div> | <div> | ||
| − | < | + | <button type="submit" name="btnOperacion" value="ALTA"> |
| − | + | Dar de alta | |
| − | + | </button> | |
| − | + | </div> | |
| − | + | ||
| − | + | </form> | |
| − | + | ||
| + | </body> | ||
</html> | </html> | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==== Arquivo consultar_estados.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 | ||
| + | ] | ||
| + | ); | ||
| + | $stmt = $conex->query(CONSULTAR_ESTADOS); | ||
| + | $estados = $stmt->fetchAll(); | ||
| − | + | } catch (PDOException $e) { | |
| − | |||
| − | |||
| − | |||
| − | |||
| + | error_log($e->getMessage()); | ||
| − | ' | + | die('Erro relacionado coa base de datos.'); |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | } catch (Throwable $e) { | |
| − | |||
| − | |||
| − | |||
| + | error_log($e->getMessage()); | ||
| + | die('Produciuse un erro na aplicación.'); | ||
| + | } | ||
| + | ?> | ||
<!DOCTYPE html> | <!DOCTYPE html> | ||
| − | <html> | + | <html lang="gl"> |
<head> | <head> | ||
| − | + | <meta charset="UTF-8"> | |
| + | <title>Xestión de estados</title> | ||
</head> | </head> | ||
<body> | <body> | ||
| − | + | ||
| − | + | <h1>Xestión de estados</h1> | |
| − | <?php | + | |
| − | < | + | <form action="alta_estado.php" method="get"> |
| − | < | + | <button type="submit">Novo estado</button> |
| − | + | </form> | |
| − | + | ||
| + | <table> | ||
| + | <thead> | ||
| + | <tr> | ||
| + | <th>Id</th> | ||
| + | <th>Descrición</th> | ||
| + | </tr> | ||
| + | </thead> | ||
| + | <tbody> | ||
| + | |||
| + | <?php foreach ($estados as $estado): ?> | ||
| + | |||
| + | <tr> | ||
| + | <td> | ||
| + | <?= htmlspecialchars( | ||
| + | (string) $estado['id'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | </td> | ||
| + | |||
| + | <td> | ||
| + | <?= htmlspecialchars( | ||
| + | $estado['descripcion'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | </td> | ||
| + | </tr> | ||
| + | |||
| + | <?php endforeach; ?> | ||
| + | |||
| + | </tbody> | ||
| + | </table> | ||
| + | |||
| + | <?php if (isset($_GET['id'])): ?> | ||
| + | |||
| + | <p> | ||
| + | Estado con id | ||
| + | <?= htmlspecialchars( | ||
| + | $_GET['id'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | dado de alta correctamente. | ||
| + | </p> | ||
| + | |||
| + | <?php endif; ?> | ||
| + | |||
| + | <?php if (isset($_GET['error'])): ?> | ||
| + | |||
| + | <p> | ||
| + | <?= htmlspecialchars( | ||
| + | $_GET['error'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | </p> | ||
| + | |||
| + | <?php endif; ?> | ||
| + | |||
</body> | </body> | ||
</html> | </html> | ||
| + | </syntaxhighlight> | ||
| − | </ | + | Neste caso <code>CONSULTAR_ESTADOS</code> non leva datos variables, polo que podemos executar directamente <code>query()</code>. |
| + | |||
| + | Se a consulta tivese datos variables deberiamos empregar <code>prepare()</code> e <code>execute()</code>. | ||
| + | |||
| + | <br /> | ||
| + | ==== Arquivo procesar_alta_estado.php ==== | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | ::<syntaxhighlight lang=" | ||
<?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, | |
| − | $conex = new | + | PASSWORD, |
| − | + | [ | |
| − | + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | |
| − | if (! | + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION |
| − | + | ] | |
| − | + | ); | |
| − | } | + | |
| − | + | if (!isset($_POST['btnOperacion']) || $_POST['btnOperacion'] !== 'ALTA') { | |
| − | + | // Podríamos lanzar unha excepción dun tipo creada por nos ou empregar => throw new RuntimeException('Operación non válida.'); | |
| − | + | header( | |
| − | + | 'Location: consultar_estados.php?error=' . | |
| − | + | urlencode('Operación non válida.') | |
| − | + | ); | |
| − | + | exit; | |
| − | + | } | |
| − | + | ||
| − | + | $descripcion = trim($_POST['descripcion'] ?? ''); | |
| − | + | ||
| − | + | $lonxitude = mb_strlen($descripcion, 'UTF-8'); | |
| − | + | if ($lonxitude < 4 || $lonxitude > 100) { | |
| − | + | ||
| + | header( | ||
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode( | ||
| + | 'A descrición debe ter entre 4 e 100 caracteres.' | ||
| + | ) | ||
| + | ); | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | $descripcion = mb_strtoupper($descripcion, 'UTF-8'); | ||
| + | |||
| + | $stmt = $conex->prepare(CONSULTAR_ESTADO_NUM); | ||
| + | $stmt->execute([ | ||
| + | ':descripcion' => $descripcion | ||
| + | ]); | ||
| + | |||
| + | $fila = $stmt->fetch(); | ||
| + | |||
| + | if ($fila !== false && (int) $fila['existe'] > 0) { | ||
| − | + | header( | |
| − | + | 'Location: consultar_estados.php?error=' . | |
| − | + | urlencode('Ese estado xa existe.') | |
| − | + | ); | |
| − | + | exit; | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| + | |||
| + | $stmt = $conex->prepare(INSERIR_ESTADO); | ||
| + | $stmt->execute([ | ||
| + | ':descripcion' => $descripcion | ||
| + | ]); | ||
| + | |||
| + | $id = (int) $conex->lastInsertId(); // O id é un campo autonumérico e normalmente o usuario non ten que saber da súa existencia polo que non sería necesario envialo de volta. | ||
| + | |||
| + | header( | ||
| + | 'Location: consultar_estados.php?id=' . | ||
| + | urlencode((string) $id) | ||
| + | ); | ||
| + | exit; | ||
| + | |||
| + | } catch (PDOException $e) { | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | header( | ||
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode('Erro relacionado coa base de datos.') | ||
| + | ); | ||
| + | exit; | ||
| + | |||
| + | } catch (Throwable $e) { | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | header( | ||
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode('Produciuse un erro na aplicación.') | ||
| + | ); | ||
| + | exit; | ||
} | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Neste exemplo debemos destacar varias ideas: | ||
| + | |||
| + | :* Os datos do formulario compróbanse de novo no servidor. | ||
| + | :* <code>trim()</code> elimina espazos ao principio e ao final. | ||
| + | :* A validación da lonxitude forma parte das regras da aplicación. | ||
| + | :* A conversión a maiúsculas é unha '''normalización''', non unha medida contra SQL Injection. | ||
| + | :* Os valores variables entran nas consultas mediante parámetros. | ||
| + | :* Os detalles das excepcións escríbense no log, non se amosan ao usuario. | ||
| + | :* Despois de <code>header('Location: ...')</code> emprégase <code>exit</code> para deter a execución. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==== Mellora: evitar duplicados tamén na base de datos ==== | ||
| + | |||
| + | A comprobación cun <code>SELECT</code> é útil para explicar o proceso, pero nun entorno concorrente dúas peticións poderían comprobar ao mesmo tempo que o valor non existe e intentar inserilo. | ||
| + | |||
| + | Se a regra da aplicación indica que non pode haber dous estados coa mesma descrición, a base de datos tamén debería garantir esa restrición mediante un índice ou restrición <code>UNIQUE</code> axeitada á colación empregada. | ||
| + | |||
| + | Por exemplo: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
| + | ALTER TABLE estados | ||
| + | ADD CONSTRAINT uq_estados_descripcion | ||
| + | UNIQUE (descripcion); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | A aplicación debe seguir tratando a posible excepción producida pola base de datos. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Alta utilizando procedementos almacenados === | ||
| + | |||
| + | Podemos encapsular parte da lóxica nun procedemento almacenado de MySQL/MariaDB. | ||
| + | |||
| + | Neste exemplo o procedemento: | ||
| + | |||
| + | :* comproba se o estado xa existe; | ||
| + | :* devolve <code>-1</code> se xa existe; | ||
| + | :* realiza o <code>INSERT</code> en caso contrario; | ||
| + | :* devolve o identificador xerado mediante <code>LAST_INSERT_ID()</code>. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==== Procedemento almacenado ==== | ||
| + | |||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
| + | DELIMITER $$ | ||
| + | |||
| + | CREATE PROCEDURE INSERIR_ESTADO_PROC( | ||
| + | IN p_descripcion VARCHAR(100) | ||
| + | ) | ||
| + | etiqueta: BEGIN | ||
| + | |||
| + | IF ( | ||
| + | SELECT COUNT(*) | ||
| + | FROM estados | ||
| + | WHERE UPPER(descripcion) = UPPER(p_descripcion) | ||
| + | ) > 0 THEN | ||
| − | + | SELECT -1 AS valor_devolto; | |
| − | + | LEAVE etiqueta; | |
| − | + | END IF; | |
| + | INSERT INTO estados (descripcion) | ||
| + | VALUES (p_descripcion); | ||
| + | |||
| + | SELECT LAST_INSERT_ID() AS valor_devolto; | ||
| + | |||
| + | END$$ | ||
| + | |||
| + | DELIMITER ; | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | <br /> | ||
| + | ==== Arquivo /var/www/confBD/bd.php ==== | ||
| + | Engadimos a chamada ao procedemento: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | define( | ||
| + | 'PROCEDEMENTO_INSERIR_ESTADO', | ||
| + | 'CALL INSERIR_ESTADO_PROC(:descripcion)' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | ===== | + | ==== Chamando ao procedemento con PDO ==== |
| + | |||
| + | '''Arquivo procesar_alta_estado_proc.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 | ||
| + | ] | ||
| + | ); | ||
| − | :* | + | if ( |
| + | !isset($_POST['btnOperacion']) || | ||
| + | $_POST['btnOperacion'] !== 'ALTA' | ||
| + | ) { | ||
| + | header( | ||
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode( | ||
| + | 'Operación non válida.' | ||
| + | ) | ||
| + | ); | ||
| + | exit; | ||
| + | /* Poderíamos empregar lanzar unha excepción | ||
| + | throw new RuntimeException( | ||
| + | 'Operación non válida.' | ||
| + | ); | ||
| + | */ | ||
| + | } | ||
| + | $descripcion = trim($_POST['descripcion'] ?? ''); | ||
| − | ''' | + | $lonxitude = mb_strlen($descripcion, 'UTF-8'); // A extensión mbstring ten que estar habilitada. Senon empregar strtoupper |
| − | + | if ($lonxitude < 4 || $lonxitude > 100) { | |
| − | + | header( | |
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode( | ||
| + | 'A descrición debe ter entre 4 e 100 caracteres.' | ||
| + | ) | ||
| + | ); | ||
| + | exit; | ||
| + | } | ||
| − | + | $descripcion = mb_strtoupper($descripcion, 'UTF-8'); // A extensión mbstring ten que estar habilitada. Senon empregar strtoupper | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | $stmt = $conex->prepare( | |
| − | + | PROCEDEMENTO_INSERIR_ESTADO | |
| − | + | ); | |
| − | |||
| − | |||
| − | |||
| − | + | $stmt->execute([ | |
| − | + | ':descripcion' => $descripcion | |
| + | ]); | ||
| − | + | $fila = $stmt->fetch(); | |
| − | + | $stmt->closeCursor(); | |
| − | |||
| − | + | if ($fila === false) { | |
| − | + | header( | |
| − | * | + | 'Location: consultar_estados.php?error=' . |
| + | urlencode('O procedemento non devolveu ningún resultado.')); | ||
| + | exit; | ||
| + | /* Poderíamos empregar lanzar unha excepción | ||
| + | throw new RuntimeException( | ||
| + | 'O procedemento non devolveu ningún resultado.' | ||
| + | ); | ||
| + | */ | ||
| + | } | ||
| + | $valorDevolto = (int) $fila['valor_devolto']; | ||
| + | if ($valorDevolto === -1) { | ||
| + | header( | ||
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode('Ese estado xa existe.') | ||
| + | ); | ||
| + | exit; | ||
| + | } | ||
| + | header( | ||
| + | 'Location: consultar_estados.php?id=' . | ||
| + | urlencode((string) $valorDevolto) | ||
| + | ); | ||
| + | exit; | ||
| − | + | } catch (PDOException $e) { | |
| − | |||
| − | |||
| − | + | error_log($e->getMessage()); | |
| − | |||
| − | + | header( | |
| − | + | 'Location: consultar_estados.php?error=' . | |
| − | + | urlencode('Erro relacionado coa base de datos.') | |
| − | + | ); | |
| − | + | exit; | |
| − | |||
| − | + | } catch (Throwable $e) { | |
| − | |||
| − | + | error_log($e->getMessage()); | |
| − | + | header( | |
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode('Produciuse un erro na aplicación.') | ||
| + | ); | ||
| + | exit; | ||
| + | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | |||
| + | Con PDO non empregamos <code>multi_query()</code>. Executamos a chamada mediante unha consulta preparada e, despois de recuperar o resultado que nos interesa, empregamos <code>closeCursor()</code> para liberar os resultados pendentes antes de executar outra orde na mesma conexión. | ||
| − | ''' | + | <br /> |
| − | ::<syntaxhighlight lang=" | + | |
| − | + | === Tratamento dos datos e caracteres especiais === | |
| + | |||
| + | Esta parte require diferenciar claramente '''tres problemas distintos''': | ||
| + | |||
| + | :# evitar SQL Injection; | ||
| + | :# validar ou normalizar datos segundo as regras da aplicación; | ||
| + | :# evitar Cross-Site Scripting (XSS) ao amosar datos nunha páxina HTML. | ||
| + | |||
| + | Non debemos mesturar estas tres operacións. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==== SQL Injection ==== | ||
| + | |||
| + | Para introducir valores variables nunha consulta SQL empregaremos consultas preparadas: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'INSERT INTO autores (nome) | ||
| + | VALUES (:nome)' | ||
| + | ); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':nome' => "O'Reilly" | ||
| + | ]); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | Non necesitamos modificar <code>O'Reilly</code> nin engadir barras invertidas. O valor envíase como dato asociado ao parámetro <code>:nome</code>. | ||
| + | Por tanto, con PDO e consultas preparadas '''non debemos empregar <code>mysqli::real_escape_string()</code>'''. | ||
| − | + | <br /> | |
| − | + | ||
| − | + | ==== Validación e normalización ==== | |
| + | |||
| + | A validación comproba se un dato cumpre as regras da aplicación. | ||
| − | + | Por exemplo: | |
| − | |||
| − | $ | + | ::<syntaxhighlight lang="php" enclose="div"> |
| − | $ | + | $descripcion = trim($_POST['descripcion'] ?? ''); |
| − | if ($ | + | if ($descripcion === '') { |
| − | + | die('A descrición é obrigatoria.'); | |
} | } | ||
| − | + | </syntaxhighlight> | |
| − | + | ||
| + | A normalización transforma un valor á representación decidida pola aplicación. | ||
| + | |||
| + | Por exemplo: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $descripcion = mb_strtoupper( | ||
| + | $descripcion, | ||
| + | 'UTF-8' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Ningunha destas operacións substitúe unha consulta preparada. | ||
| + | |||
| + | <br /> | ||
| + | ==== Saída HTML e XSS ==== | ||
| − | + | Se un dato se vai inserir como texto nun documento HTML, debemos escapar a saída: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | echo htmlspecialchars( | |
| − | + | $fila['descripcion'], | |
| − | + | ENT_QUOTES | ENT_SUBSTITUTE, | |
| − | + | 'UTF-8' | |
| − | + | ); | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | Isto debe facerse '''ao amosar o dato en HTML''', non necesariamente antes de gardalo na base de datos. | ||
| − | + | Por exemplo, se un usuario introduce: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | ::<syntaxhighlight lang="html" enclose="div"> | ||
| + | <script>alert('ola')</script> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | podemos gardar o texto orixinal se as regras da aplicación o permiten. Cando o amosemos nun contexto HTML aplicaremos <code>htmlspecialchars()</code> para que o navegador o trate como texto e non como marcado executable. | ||
| − | + | '''IMPORTANTE:''' o escapado depende do contexto de saída. <code>htmlspecialchars()</code> é apropiado para texto HTML, pero outros contextos como JavaScript, URLs ou atributos especiais poden requirir tratamentos diferentes. | |
| − | + | <br /> | |
| − | + | === Transaccións === | |
| − | * | + | * Se unha operación lóxica require varias modificacións na base de datos e todas deben completarse ou cancelarse xuntas, debemos empregar unha transacción. |
| − | + | : Isto será [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_TRANSACCIONS explicado posteriormente nun punto da wiki]. | |
| − | : | ||
| − | + | <br /> | |
| − | + | === mysqli como ampliación === | |
| − | |||
| − | |||
| − | + | PDO será a opción principal nos apuntes, pero PHP tamén dispón da extensión <code>mysqli</code> para traballar especificamente con MySQL/MariaDB. | |
| − | |||
| − | |||
| − | + | Se empregamos <code>mysqli</code>, tamén debemos utilizar consultas preparadas cando existan datos variables. | |
| − | |||
| − | |||
| + | <br /> | ||
| + | |||
| + | ==== Conexión con mysqli ==== | ||
| + | |||
| + | Exemplo equivalente de conexión: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | <?php | ||
| + | |||
| + | try { | ||
| − | + | require '/var/www/confBD/bd.php'; | |
| − | |||
| + | mysqli_report( | ||
| + | MYSQLI_REPORT_ERROR | | ||
| + | MYSQLI_REPORT_STRICT | ||
| + | ); | ||
| + | $conex = new mysqli( | ||
| + | SERVIDOR, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | BD | ||
| + | ); | ||
| + | $conex->set_charset('utf8mb4'); | ||
| − | + | // Operacións coa base de datos... | |
| − | |||
| − | + | } catch (mysqli_sql_exception $e) { | |
| + | error_log($e->getMessage()); | ||
| − | + | die('Erro relacionado coa base de datos.'); | |
| − | |||
| − | |||
| − | |||
| − | + | } catch (Throwable $e) { | |
| − | + | error_log($e->getMessage()); | |
| − | |||
| − | |||
| − | + | die('Produciuse un erro na aplicación.'); | |
| − | |||
| − | |||
} | } | ||
| + | </syntaxhighlight> | ||
| + | <br /> | ||
| − | + | ==== INSERT preparado con mysqli ==== | |
| − | / | + | Se no arquivo <code>bd.php</code> temos: |
| − | + | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | define( | |
| + | 'INSERIR_ESTADO_MYSQLI', | ||
| + | 'INSERT INTO estados (descripcion) | ||
| + | VALUES (?)' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | podemos facer: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $stmt = $conex->prepare( | ||
| + | INSERIR_ESTADO_MYSQLI | ||
| + | ); | ||
| + | |||
| + | $stmt->bind_param( | ||
| + | 's', | ||
| + | $descripcion | ||
| + | ); | ||
| + | |||
| + | $stmt->execute(); | ||
| + | |||
| + | $id = $conex->insert_id; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | A letra <code>s</code> indica que o parámetro é unha cadea. | ||
| + | |||
| + | Outros tipos habituais son: | ||
| + | |||
| + | :* <code>i</code>: enteiro. | ||
| + | :* <code>d</code>: número real. | ||
| + | :* <code>s</code>: cadea. | ||
| + | :* <code>b</code>: dato binario. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==== real_escape_string() ==== | ||
| + | |||
| + | <code>mysqli::real_escape_string()</code> segue existindo, pero '''non será a técnica principal dos apuntes para introducir datos variables nas consultas'''. | ||
| + | |||
| + | Empregaremos preferentemente: | ||
| − | $ | + | ::<syntaxhighlight lang="php" enclose="div"> |
| + | $stmt = $conex->prepare( | ||
| + | 'INSERT INTO autores (nome) | ||
| + | VALUES (?)' | ||
| + | ); | ||
| − | + | $stmt->bind_param( | |
| − | + | 's', | |
| − | + | $nome | |
| − | + | ); | |
| − | $ | + | $stmt->execute(); |
| − | |||
</syntaxhighlight> | </syntaxhighlight> | ||
| − | + | Isto evita ter que construír manualmente a consulta SQL concatenando ou interpolando valores. | |
| − | + | <br /> | |
| + | === Exercicios propostos === | ||
| + | * De todos os exercicios fai dúas versións, unha empregando a orde SQL INSERT e outra chamando a un procedemento almacenado de Mysql. | ||
| + | <br /> | ||
| + | ==== Alta de Autores ==== | ||
| + | * Crea as páxinas necesarias para poder listar ordenados alfabáticamente e dar de alta novos autores, tendo en conta o seguinte: | ||
| + | :* O formulario de alta estará conformado por dous campos: nome e apelidos. Cada un deles será obrigatorio e terá de tamaño mínimo 3 caracteres e máximo o marcado polo deseño da táboa en cada un deses campos. | ||
| + | ::* Os campos nome e apelidos, estarán definidos nun arquivo separado como se fixo no exercicio de estados. | ||
| + | ::* Deben validarse tanto no cliente (empregando required, maxlength e minlength) como no servidor. | ||
| + | :* O campo id é a clave autonumérica e non debe aparecer no formulario de alta. Para practicar ides enviar o di xerado á páxina de listado cando se dea de alta un novo autor (como se fixo no exercicio dos estados). | ||
| + | :* Deberedes comprobar que non exista outro autor có mesmo nome e apelidos. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==== Alta de Libros ==== | ||
| + | |||
| + | * Crea as páxinas necesarias para poder listar ordenados alfabáticamente e dar de alta novos libros, tendo en conta o seguinte: | ||
| + | :* O formulario de alta estará conformado por o campo: titulo. Será obrigatorio e terá de tamaño mínimo 3 caracteres e máximo o marcado polo deseño da táboa. | ||
| + | ::* O campo do formulario estará definido nun arquivo separado como se fixo no exercicio de estados. | ||
| + | ::* Deben validarse tanto no cliente (empregando required, maxlength e minlength) como no servidor. | ||
| + | :* O campo id é a clave autonumérica e non debe aparecer no formulario de alta. Para practicar ides enviar o di xerado á páxina de listado cando se dea de alta un novo libroi (como se fixo no exercicio dos estados). | ||
| + | :* Deberedes comprobar que non exista outro libro có mesmo título. | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | ==== Alta de Exemplares ==== | ||
| + | |||
| + | * Crea unha páxina de listado exemplares. | ||
| + | : Dita páxina terá unha combo (select) cós libros creados e ordenados alfabeticamente. Ao seleccionar un libro deben aparecer os exemplares ordenados alfabeticamente, de dito libro. O libro seleccionado debe manterse seleccionado na combo. | ||
| + | * Engade un botón Novo Exemplar que cargue a páxina de alta exemplares. Dita páxina amosará o título do libro que estamos a dar de alta e terás que gardar de forma oculta o id do libro. | ||
| + | : O formulario de alta exemplar ten que amosar os seguintes campos definidos non arquivo separado como no exercicio dos estados: | ||
| + | :* Unha combo (select) cós estados | ||
| + | :* prezo: decimal (dous díxitos), obrigatorio. Terás que validar que sexa positivo, maior que cero e menor que 6000. | ||
| + | :* imaxe: Polo de agora séra unha cadea obrigatoria (veremos como subir imaxes ao servidor). | ||
| + | :* id_exemplar: debe amosar o seguinte id que exista para ese libro. | ||
| + | |||
| + | * Ao dar de alta se terá que comprobar que o id_libro, id_exemplar non esté xa engadido na base de datos. | ||
| + | : Cargará a páxina de listado de libros. | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | === Resumo === | ||
| − | < | + | * En PHP 8.5 empregaremos '''PDO como mecanismo principal''' de acceso a MySQL/MariaDB. |
| + | * As credenciais e as consultas gardaranse en <code>/var/www/confBD/bd.php</code>, fóra de <code>/var/www/html</code>. | ||
| + | * Para consultas con datos variables empregaremos <code>prepare()</code> e <code>execute()</code>. | ||
| + | * Non concatenaremos datos recibidos do usuario dentro dunha orde SQL. | ||
| + | * Para obter un identificador <code>AUTO_INCREMENT</code> empregaremos <code>PDO::lastInsertId()</code>. | ||
| + | * Para coñecer as filas afectadas por un <code>INSERT</code>, <code>UPDATE</code> ou <code>DELETE</code> podemos empregar <code>PDOStatement::rowCount()</code>. | ||
| + | * Non empregaremos <code>rowCount()</code> como método portable para contar os resultados dun <code>SELECT</code>. | ||
| + | * As excepcións detalladas rexistraranse no log; ao usuario amosaráselle unha mensaxe xenérica. | ||
| + | * <code>htmlspecialchars()</code> aplicarase ao xerar saída HTML cando sexa necesario evitar que un dato se interprete como marcado. | ||
| + | * A validación, a normalización, a prevención de SQL Injection e o escapado da saída son problemas diferentes. | ||
| + | * <code>mysqli</code> explicarase como alternativa adicional, tamén empregando consultas preparadas. | ||
Revisión actual del 18:09 8 oct 2026
Sumario
Introdución
Nesta unidade veremos como realizar operacións de alta sobre unha base de datos MySQL/MariaDB desde PHP 8.5.
O mecanismo principal empregado nos apuntes será PDO (PHP Data Objects). PDO proporciona unha interface uniforme para traballar con diferentes sistemas xestores de bases de datos e permite empregar consultas preparadas con parámetros.
Ao final da unidade inclúese tamén unha explicación equivalente empregando mysqli, como ampliación e para poder interpretar código existente escrito con esta extensión.
- Requirimentos previos:
- Ter creada a base de datos indicada nunha sección anterior.
- Ter gardados os parámetros de conexión nun arquivo separado, fóra de
/var/www/html. - Ter instalada e habilitada a extensión
pdo_mysql. - Os exemplos están pensados para PHP 8.5.
- CONSIDERACIÓNS:
- Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos)
- Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como vimos nun punto anterior desta sección.
- Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas.
- En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes.
- Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...).
- Lembrar que tamén podedes activar o debugger como está indicado nesta sección da wiki.
Organización dos exemplos
Nos exemplos completos empregaremos o seguinte esquema.
No arquivo /var/www/confBD/bd.php, que se atopa fóra da ruta pública /var/www/html, gardaremos os datos da conexión e as consultas que se vaian empregar:
<?php define('SERVIDOR', 'localhost'); define('USUARIO', 'user_php'); define('PASSWORD', 'user_php'); define('BD', 'php'); define( 'NOME_DA_CONSULTA', 'SELECT col1, col2 FROM taboa ORDER BY col1' );
Nas páxinas PHP que accedan á base de datos empregaremos como base o seguinte esquema:
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); // Operacións coa base de datos... } catch (PDOException $e) { // Rexistrar o erro nun log. // Nunha aplicación real tamén poderían empregarse // ferramentas como Monolog. 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.'); }
IMPORTANTE:
- O usuario da aplicación non debe recibir detalles internos da excepción, nomes de táboas, rutas do servidor nin credenciais.
- Durante o desenvolvemento podemos consultar os logs do servidor para localizar o problema.
- En PHP 8 PDO emprega excepcións por defecto, pero nos exemplos indicamos explicitamente
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTIONpara que o comportamento quede claro. - Indicamos tamén
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, polo que os métodosfetch()efetchAll()devolverán por defecto arrays asociativos.
Consultas preparadas
Sempre que unha consulta conteña datos variables procedentes do usuario ou da aplicación, empregaremos unha consulta preparada.
Por exemplo, non construiremos unha consulta desta forma:
$consulta = sprintf( "INSERT INTO estados (descripcion) VALUES ('%s')", $descripcion );
Empregaremos un parámetro:
$stmt = $conex->prepare( 'INSERT INTO estados (descripcion) VALUES (:descripcion)' ); $stmt->execute([ ':descripcion' => $descripcion ]);
Os parámetros representan valores. Non se poden utilizar para substituír nomes de táboas, nomes de columnas, palabras clave SQL ou fragmentos arbitrarios dunha consulta.
Alta de rexistros: INSERT
Para dar de alta novos rexistros nunha base de datos empregamos a instrución SQL INSERT.
Por exemplo:
INSERT INTO TABOA_1 (col1, col2) VALUES (123, 'Texto de proba');
Desde PHP empregaremos normalmente unha consulta preparada.
Alta empregando PDO
Supoñamos a seguinte táboa:
CREATE TABLE TABOA_1 ( col1 INT PRIMARY KEY AUTO_INCREMENT, col2 VARCHAR(100) NOT NULL );
Neste caso non indicaremos col1 no INSERT, xa que MySQL/MariaDB xerará automaticamente o seu valor.
Arquivo /var/www/confBD/bd.php
<?php define('SERVIDOR', 'localhost'); define('USUARIO', 'user_php'); define('PASSWORD', 'user_php'); define('BD', 'php'); define( 'INSERIR_TABOA_1', 'INSERT INTO TABOA_1 (col2) VALUES (:col2)' );
Arquivo inserir_taboa_1.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 ] ); $valor = 'Texto de proba'; $stmt = $conex->prepare(INSERIR_TABOA_1); $stmt->execute([ ':col2' => $valor ]); echo 'Rexistro dado de alta correctamente.'; } 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.'); }
Neste exemplo:
prepare()prepara a orde SQL.:col2é un parámetro con nome.execute()envía o valor asociado ao parámetro e executa a orde.- Non é necesario engadir comiñas manualmente ao valor nin empregar
real_escape_string().
Número de filas afectadas
Despois dun INSERT, podemos consultar o número de filas afectadas mediante rowCount() sobre o obxecto PDOStatement:
$stmt->execute([ ':col2' => $valor ]); $numRexistros = $stmt->rowCount(); if ($numRexistros === 1) { echo 'Rexistro dado de alta correctamente.'; }
Nun INSERT dunha única fila o normal é obter o valor 1 se a operación se realizou.
Nota: non debemos utilizar rowCount() para determinar de forma portable o número de filas devoltas por un SELECT. Para un SELECT recuperaremos os datos con fetch(), fetchColumn ou fetchAll().
Obtendo o valor dun campo autonumérico
Cando unha táboa ten unha clave primaria AUTO_INCREMENT, MySQL/MariaDB xera automaticamente o identificador da nova fila.
Non debemos inserir unha fila e realizar despois:
SELECT MAX(id) FROM taboa;
Nunha aplicación con varios usuarios outra conexión podería inserir unha fila entre ambas as operacións.
Con PDO empregaremos:
$id = $conex->lastInsertId();
lastInsertId() devolve o identificador asociado á última inserción realizada pola conexión.
Exemplo:
$stmt = $conex->prepare(INSERIR_TABOA_1); $stmt->execute([ ':col2' => $valor ]); $id = $conex->lastInsertId(); echo 'Novo rexistro co id ' . htmlspecialchars($id, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
Nota: PDO::lastInsertId() devolve normalmente unha cadea. Se necesitamos tratar o identificador como enteiro podemos convertelo explicitamente:
$id = (int) $conex->lastInsertId();
Caso práctico: mantemento da táboa ESTADOS
Imos facer unha pequena parte do mantemento da táboa ESTADOS.
Supoñamos:
CREATE TABLE estados ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, descripcion VARCHAR(100) NOT NULL );
Crearemos:
consultar_estados.php: amosa os estados existentes.alta_estado.php: amosa o formulario de alta.campos_estado.php: contén os campos reutilizables do formulario. Isto quere decir que os campos van ser os mesmos se damos de alta ou se modificamos un rexistro, polo que nas dúas operacións (alta / modificar) podemos empregar o mesmo include cós campos tendo en conta que no caso de modificación teremos que ter o código php necesario para cargar o dato do rexistro a modificar.procesar_alta_estado.php: valida os datos e realiza oINSERT./var/www/confBD/bd.php: contén credenciais e consultas SQL.
Neste punto do curso enviaremos o resultado da operación mediante parámetros na URL. Máis adiante, cando se estuden as sesións, será preferible empregar unha mensaxe gardada na sesión e facer o patrón POST/Redirect/GET.
Arquivo /var/www/confBD/bd.php
<?php define('SERVIDOR', 'localhost'); define('USUARIO', 'user_php'); define('PASSWORD', 'user_php'); define('BD', 'php'); define( 'CONSULTAR_ESTADOS', 'SELECT id, descripcion FROM estados ORDER BY descripcion' ); define( 'CONSULTAR_ESTADO_NUM', 'SELECT COUNT(*) AS existe FROM estados WHERE UPPER(descripcion) = UPPER(:descripcion)' ); define( 'INSERIR_ESTADO', 'INSERT INTO estados (descripcion) VALUES (:descripcion)' );
Fixarse en que:
- As credenciais non están nun arquivo accesible directamente desde a web.
- As consultas SQL están centralizadas.
- Os datos variables empréganse mediante parámetros como
:descripcion. - Non se introduce directamente ningún dato do formulario dentro da cadea SQL.
Arquivo campos_estado.php
<label for="descripcion">Descrición:</label> <input type="text" id="descripcion" name="descripcion" minlength="4" maxlength="100" required >
A validación HTML mellora a experiencia do usuario, pero non substitúe a validación no servidor. Unha petición HTTP pode enviarse sen utilizar o formulario.
Arquivo alta_estado.php
<!DOCTYPE html> <html lang="gl"> <head> <meta charset="UTF-8"> <title>Alta de estado</title> </head> <body> <h1>Alta de estado</h1> <form method="post" action="procesar_alta_estado.php"> <?php require 'campos_estado.php'; ?> <div> <button type="submit" name="btnOperacion" value="ALTA"> Dar de alta </button> </div> </form> </body> </html>
Arquivo consultar_estados.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 ] ); $stmt = $conex->query(CONSULTAR_ESTADOS); $estados = $stmt->fetchAll(); } catch (PDOException $e) { error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); } ?> <!DOCTYPE html> <html lang="gl"> <head> <meta charset="UTF-8"> <title>Xestión de estados</title> </head> <body> <h1>Xestión de estados</h1> <form action="alta_estado.php" method="get"> <button type="submit">Novo estado</button> </form> <table> <thead> <tr> <th>Id</th> <th>Descrición</th> </tr> </thead> <tbody> <?php foreach ($estados as $estado): ?> <tr> <td> <?= htmlspecialchars( (string) $estado['id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </td> <td> <?= htmlspecialchars( $estado['descripcion'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php if (isset($_GET['id'])): ?> <p> Estado con id <?= htmlspecialchars( $_GET['id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> dado de alta correctamente. </p> <?php endif; ?> <?php if (isset($_GET['error'])): ?> <p> <?= htmlspecialchars( $_GET['error'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </p> <?php endif; ?> </body> </html>
Neste caso CONSULTAR_ESTADOS non leva datos variables, polo que podemos executar directamente query().
Se a consulta tivese datos variables deberiamos empregar prepare() e execute().
Arquivo procesar_alta_estado.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 ] ); if (!isset($_POST['btnOperacion']) || $_POST['btnOperacion'] !== 'ALTA') { // Podríamos lanzar unha excepción dun tipo creada por nos ou empregar => throw new RuntimeException('Operación non válida.'); header( 'Location: consultar_estados.php?error=' . urlencode('Operación non válida.') ); exit; } $descripcion = trim($_POST['descripcion'] ?? ''); $lonxitude = mb_strlen($descripcion, 'UTF-8'); if ($lonxitude < 4 || $lonxitude > 100) { header( 'Location: consultar_estados.php?error=' . urlencode( 'A descrición debe ter entre 4 e 100 caracteres.' ) ); exit; } $descripcion = mb_strtoupper($descripcion, 'UTF-8'); $stmt = $conex->prepare(CONSULTAR_ESTADO_NUM); $stmt->execute([ ':descripcion' => $descripcion ]); $fila = $stmt->fetch(); if ($fila !== false && (int) $fila['existe'] > 0) { header( 'Location: consultar_estados.php?error=' . urlencode('Ese estado xa existe.') ); exit; } $stmt = $conex->prepare(INSERIR_ESTADO); $stmt->execute([ ':descripcion' => $descripcion ]); $id = (int) $conex->lastInsertId(); // O id é un campo autonumérico e normalmente o usuario non ten que saber da súa existencia polo que non sería necesario envialo de volta. header( 'Location: consultar_estados.php?id=' . urlencode((string) $id) ); exit; } catch (PDOException $e) { error_log($e->getMessage()); header( 'Location: consultar_estados.php?error=' . urlencode('Erro relacionado coa base de datos.') ); exit; } catch (Throwable $e) { error_log($e->getMessage()); header( 'Location: consultar_estados.php?error=' . urlencode('Produciuse un erro na aplicación.') ); exit; }
Neste exemplo debemos destacar varias ideas:
- Os datos do formulario compróbanse de novo no servidor.
trim()elimina espazos ao principio e ao final.- A validación da lonxitude forma parte das regras da aplicación.
- A conversión a maiúsculas é unha normalización, non unha medida contra SQL Injection.
- Os valores variables entran nas consultas mediante parámetros.
- Os detalles das excepcións escríbense no log, non se amosan ao usuario.
- Despois de
header('Location: ...')emprégaseexitpara deter a execución.
Mellora: evitar duplicados tamén na base de datos
A comprobación cun SELECT é útil para explicar o proceso, pero nun entorno concorrente dúas peticións poderían comprobar ao mesmo tempo que o valor non existe e intentar inserilo.
Se a regra da aplicación indica que non pode haber dous estados coa mesma descrición, a base de datos tamén debería garantir esa restrición mediante un índice ou restrición UNIQUE axeitada á colación empregada.
Por exemplo:
ALTER TABLE estados ADD CONSTRAINT uq_estados_descripcion UNIQUE (descripcion);
A aplicación debe seguir tratando a posible excepción producida pola base de datos.
Alta utilizando procedementos almacenados
Podemos encapsular parte da lóxica nun procedemento almacenado de MySQL/MariaDB.
Neste exemplo o procedemento:
- comproba se o estado xa existe;
- devolve
-1se xa existe; - realiza o
INSERTen caso contrario; - devolve o identificador xerado mediante
LAST_INSERT_ID().
Procedemento almacenado
DELIMITER $$ CREATE PROCEDURE INSERIR_ESTADO_PROC( IN p_descripcion VARCHAR(100) ) etiqueta: BEGIN IF ( SELECT COUNT(*) FROM estados WHERE UPPER(descripcion) = UPPER(p_descripcion) ) > 0 THEN SELECT -1 AS valor_devolto; LEAVE etiqueta; END IF; INSERT INTO estados (descripcion) VALUES (p_descripcion); SELECT LAST_INSERT_ID() AS valor_devolto; END$$ DELIMITER ;
Arquivo /var/www/confBD/bd.php
Engadimos a chamada ao procedemento:
define( 'PROCEDEMENTO_INSERIR_ESTADO', 'CALL INSERIR_ESTADO_PROC(:descripcion)' );
Chamando ao procedemento con PDO
Arquivo procesar_alta_estado_proc.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 ] ); if ( !isset($_POST['btnOperacion']) || $_POST['btnOperacion'] !== 'ALTA' ) { header( 'Location: consultar_estados.php?error=' . urlencode( 'Operación non válida.' ) ); exit; /* Poderíamos empregar lanzar unha excepción throw new RuntimeException( 'Operación non válida.' ); */ } $descripcion = trim($_POST['descripcion'] ?? ''); $lonxitude = mb_strlen($descripcion, 'UTF-8'); // A extensión mbstring ten que estar habilitada. Senon empregar strtoupper if ($lonxitude < 4 || $lonxitude > 100) { header( 'Location: consultar_estados.php?error=' . urlencode( 'A descrición debe ter entre 4 e 100 caracteres.' ) ); exit; } $descripcion = mb_strtoupper($descripcion, 'UTF-8'); // A extensión mbstring ten que estar habilitada. Senon empregar strtoupper $stmt = $conex->prepare( PROCEDEMENTO_INSERIR_ESTADO ); $stmt->execute([ ':descripcion' => $descripcion ]); $fila = $stmt->fetch(); $stmt->closeCursor(); if ($fila === false) { header( 'Location: consultar_estados.php?error=' . urlencode('O procedemento non devolveu ningún resultado.')); exit; /* Poderíamos empregar lanzar unha excepción throw new RuntimeException( 'O procedemento non devolveu ningún resultado.' ); */ } $valorDevolto = (int) $fila['valor_devolto']; if ($valorDevolto === -1) { header( 'Location: consultar_estados.php?error=' . urlencode('Ese estado xa existe.') ); exit; } header( 'Location: consultar_estados.php?id=' . urlencode((string) $valorDevolto) ); exit; } catch (PDOException $e) { error_log($e->getMessage()); header( 'Location: consultar_estados.php?error=' . urlencode('Erro relacionado coa base de datos.') ); exit; } catch (Throwable $e) { error_log($e->getMessage()); header( 'Location: consultar_estados.php?error=' . urlencode('Produciuse un erro na aplicación.') ); exit; }
Con PDO non empregamos multi_query(). Executamos a chamada mediante unha consulta preparada e, despois de recuperar o resultado que nos interesa, empregamos closeCursor() para liberar os resultados pendentes antes de executar outra orde na mesma conexión.
Tratamento dos datos e caracteres especiais
Esta parte require diferenciar claramente tres problemas distintos:
- evitar SQL Injection;
- validar ou normalizar datos segundo as regras da aplicación;
- evitar Cross-Site Scripting (XSS) ao amosar datos nunha páxina HTML.
Non debemos mesturar estas tres operacións.
SQL Injection
Para introducir valores variables nunha consulta SQL empregaremos consultas preparadas:
$stmt = $conex->prepare( 'INSERT INTO autores (nome) VALUES (:nome)' ); $stmt->execute([ ':nome' => "O'Reilly" ]);
Non necesitamos modificar O'Reilly nin engadir barras invertidas. O valor envíase como dato asociado ao parámetro :nome.
Por tanto, con PDO e consultas preparadas non debemos empregar mysqli::real_escape_string().
Validación e normalización
A validación comproba se un dato cumpre as regras da aplicación.
Por exemplo:
$descripcion = trim($_POST['descripcion'] ?? ''); if ($descripcion === '') { die('A descrición é obrigatoria.'); }
A normalización transforma un valor á representación decidida pola aplicación.
Por exemplo:
$descripcion = mb_strtoupper( $descripcion, 'UTF-8' );
Ningunha destas operacións substitúe unha consulta preparada.
Saída HTML e XSS
Se un dato se vai inserir como texto nun documento HTML, debemos escapar a saída:
echo htmlspecialchars( $fila['descripcion'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
Isto debe facerse ao amosar o dato en HTML, non necesariamente antes de gardalo na base de datos.
Por exemplo, se un usuario introduce:
<script>alert('ola')</script>
podemos gardar o texto orixinal se as regras da aplicación o permiten. Cando o amosemos nun contexto HTML aplicaremos htmlspecialchars() para que o navegador o trate como texto e non como marcado executable.
IMPORTANTE: o escapado depende do contexto de saída. htmlspecialchars() é apropiado para texto HTML, pero outros contextos como JavaScript, URLs ou atributos especiais poden requirir tratamentos diferentes.
Transaccións
- Se unha operación lóxica require varias modificacións na base de datos e todas deben completarse ou cancelarse xuntas, debemos empregar unha transacción.
- Isto será explicado posteriormente nun punto da wiki.
mysqli como ampliación
PDO será a opción principal nos apuntes, pero PHP tamén dispón da extensión mysqli para traballar especificamente con MySQL/MariaDB.
Se empregamos mysqli, tamén debemos utilizar consultas preparadas cando existan datos variables.
Conexión con mysqli
Exemplo equivalente de 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 coa base de datos... } catch (mysqli_sql_exception $e) { error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); }
INSERT preparado con mysqli
Se no arquivo bd.php temos:
define( 'INSERIR_ESTADO_MYSQLI', 'INSERT INTO estados (descripcion) VALUES (?)' );
podemos facer:
$stmt = $conex->prepare( INSERIR_ESTADO_MYSQLI ); $stmt->bind_param( 's', $descripcion ); $stmt->execute(); $id = $conex->insert_id;
A letra s indica que o parámetro é unha cadea.
Outros tipos habituais son:
i: enteiro.d: número real.s: cadea.b: dato binario.
real_escape_string()
mysqli::real_escape_string() segue existindo, pero non será a técnica principal dos apuntes para introducir datos variables nas consultas.
Empregaremos preferentemente:
$stmt = $conex->prepare( 'INSERT INTO autores (nome) VALUES (?)' ); $stmt->bind_param( 's', $nome ); $stmt->execute();
Isto evita ter que construír manualmente a consulta SQL concatenando ou interpolando valores.
Exercicios propostos
- De todos os exercicios fai dúas versións, unha empregando a orde SQL INSERT e outra chamando a un procedemento almacenado de Mysql.
Alta de Autores
- Crea as páxinas necesarias para poder listar ordenados alfabáticamente e dar de alta novos autores, tendo en conta o seguinte:
- O formulario de alta estará conformado por dous campos: nome e apelidos. Cada un deles será obrigatorio e terá de tamaño mínimo 3 caracteres e máximo o marcado polo deseño da táboa en cada un deses campos.
- Os campos nome e apelidos, estarán definidos nun arquivo separado como se fixo no exercicio de estados.
- Deben validarse tanto no cliente (empregando required, maxlength e minlength) como no servidor.
- O campo id é a clave autonumérica e non debe aparecer no formulario de alta. Para practicar ides enviar o di xerado á páxina de listado cando se dea de alta un novo autor (como se fixo no exercicio dos estados).
- Deberedes comprobar que non exista outro autor có mesmo nome e apelidos.
Alta de Libros
- Crea as páxinas necesarias para poder listar ordenados alfabáticamente e dar de alta novos libros, tendo en conta o seguinte:
- O formulario de alta estará conformado por o campo: titulo. Será obrigatorio e terá de tamaño mínimo 3 caracteres e máximo o marcado polo deseño da táboa.
- O campo do formulario estará definido nun arquivo separado como se fixo no exercicio de estados.
- Deben validarse tanto no cliente (empregando required, maxlength e minlength) como no servidor.
- O campo id é a clave autonumérica e non debe aparecer no formulario de alta. Para practicar ides enviar o di xerado á páxina de listado cando se dea de alta un novo libroi (como se fixo no exercicio dos estados).
- Deberedes comprobar que non exista outro libro có mesmo título.
Alta de Exemplares
- Crea unha páxina de listado exemplares.
- Dita páxina terá unha combo (select) cós libros creados e ordenados alfabeticamente. Ao seleccionar un libro deben aparecer os exemplares ordenados alfabeticamente, de dito libro. O libro seleccionado debe manterse seleccionado na combo.
- Engade un botón Novo Exemplar que cargue a páxina de alta exemplares. Dita páxina amosará o título do libro que estamos a dar de alta e terás que gardar de forma oculta o id do libro.
- O formulario de alta exemplar ten que amosar os seguintes campos definidos non arquivo separado como no exercicio dos estados:
- Unha combo (select) cós estados
- prezo: decimal (dous díxitos), obrigatorio. Terás que validar que sexa positivo, maior que cero e menor que 6000.
- imaxe: Polo de agora séra unha cadea obrigatoria (veremos como subir imaxes ao servidor).
- id_exemplar: debe amosar o seguinte id que exista para ese libro.
- Ao dar de alta se terá que comprobar que o id_libro, id_exemplar non esté xa engadido na base de datos.
- Cargará a páxina de listado de libros.
Resumo
- En PHP 8.5 empregaremos PDO como mecanismo principal de acceso a MySQL/MariaDB.
- As credenciais e as consultas gardaranse en
/var/www/confBD/bd.php, fóra de/var/www/html. - Para consultas con datos variables empregaremos
prepare()eexecute(). - Non concatenaremos datos recibidos do usuario dentro dunha orde SQL.
- Para obter un identificador
AUTO_INCREMENTempregaremosPDO::lastInsertId(). - Para coñecer as filas afectadas por un
INSERT,UPDATEouDELETEpodemos empregarPDOStatement::rowCount(). - Non empregaremos
rowCount()como método portable para contar os resultados dunSELECT. - As excepcións detalladas rexistraranse no log; ao usuario amosaráselle unha mensaxe xenérica.
htmlspecialchars()aplicarase ao xerar saída HTML cando sexa necesario evitar que un dato se interprete como marcado.- A validación, a normalización, a prevención de SQL Injection e o escapado da saída son problemas diferentes.
mysqliexplicarase como alternativa adicional, tamén empregando consultas preparadas.