Diferencia entre revisiones de «PHP UD3 OP MODIFICAR»
| Línea 649: | Línea 649: | ||
$_POST['btnModificarEstado'] !== 'MODIFICAR' | $_POST['btnModificarEstado'] !== 'MODIFICAR' | ||
) { | ) { | ||
| + | |||
| + | header( | ||
| + | 'Location: consultar_estados.php?error=' . | ||
| + | urlencode('O identificador non é válido.') | ||
| + | ); | ||
| + | exit; | ||
| + | /* Outra forma de enviar o erro | ||
throw new RuntimeException( | throw new RuntimeException( | ||
'Operación non válida.' | 'Operación non válida.' | ||
); | ); | ||
| + | */ | ||
} | } | ||
Revisión del 09:36 6 oct 2026
Sumario
- 1 Introdución
- 2 Modificación de rexistros: UPDATE
- 3 Caso práctico: modificación dun estado
- 3.1 Arquivo /var/www/confBD/bd.php
- 3.2 Modificación de consultar_estados.php
- 3.3 Modificación de campos_estado.php
- 3.4 Arquivo editar_estado.php
- 3.5 Por que volvemos enviar o identificador?
- 3.6 Arquivo procesar_editar_estado.php
- 3.7 Validación da descrición
- 3.8 Normalización a maiúsculas
- 3.9 Comportamento de rowCount() nun UPDATE
- 4 Evitando valores duplicados
- 5 Transaccións
- 6 Modificación mediante un procedemento almacenado
- 7 mysqli como ampliación
- 8 Resumo
Introdución
Nesta unidade veremos como realizar operacións de modificación sobre unha base de datos MySQL/MariaDB desde PHP 8.5.
O mecanismo principal empregado nos apuntes será PDO (PHP Data Objects). Ao final inclúese tamén unha explicación equivalente empregando mysqli como ampliación.
- Requirimentos previos:
- Ter creada a base de datos indicada nunha sección anterior.
- Ter gardados os parámetros de conexión nun arquivo separado, fóra da ruta
/var/www/html. - Ter instalada e habilitada a extensión
pdo_mysql. - Para os exemplos que traballan con cadeas UTF-8 empregaremos tamén a extensión
mbstring. - Os exemplos están pensados para PHP 8.5.
- Partiremos do código realizado anteriormente nas seccións de alta e baixa.
- NOTA IMPORTANTE: cando empregamos datos procedentes dun formulario debemos validalos no servidor e utilizar consultas preparadas para incorporalos ás ordes SQL.
- A protección fronte a ataques CSRF (Cross-Site Request Forgery) incorporarase cando se estuden as sesións. Unha petición
POSTpor si soa non evita un ataque CSRF.
- CONSIDERACIÓNS:
- Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos)
- Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como vimos nun punto anterior desta sección.
- Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas.
- En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes.
- Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...).
- Lembrar que tamén podedes activar o debugger como está indicado nesta sección da wiki.
Organización dos exemplos
Nos exemplos completos empregaremos o mesmo esquema que nas seccións anteriores.
No arquivo /var/www/confBD/bd.php, situado fóra da ruta pública /var/www/html, gardaremos os datos da conexión e as consultas que se vaian empregar:
<?php define('SERVIDOR', 'localhost'); define('USUARIO', 'user_php'); define('PASSWORD', 'user_php'); define('BD', 'php'); define( 'NOME_DA_CONSULTA', 'SELECT col1, col2 FROM taboa ORDER BY col1' );
Nas páxinas PHP que accedan á base de datos empregaremos como base o seguinte esquema:
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); // Operacións coa base de datos... } catch (PDOException $e) { error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); }
Modificación de rexistros: UPDATE
- A modificación de datos realízase mediante a orde SQL
UPDATE.
Por exemplo:
UPDATE estados SET descripcion = 'NOVO VALOR' WHERE id = 10;
- A orde
UPDATEindicará:
- a táboa que queremos modificar;
- as columnas e os novos valores mediante
SET; - as filas que se deben modificar mediante
WHERE.
- IMPORTANTE: debemos ter especial coidado coa cláusula
WHERE.
A seguinte orde:
UPDATE estados SET descripcion = 'NOVO VALOR';
modificaría todas as filas da táboa.
- Normalmente non modificaremos a clave primaria. O habitual é empregar a clave primaria para identificar a fila e modificar un ou varios dos seus atributos.
- Se unha clave que se modifica está referenciada mediante unha clave foránea, o comportamento dependerá da acción definida na relación, por exemplo
ON UPDATE CASCADEouON UPDATE RESTRICT.
UPDATE empregando PDO
Sempre que a orde conteña valores variables empregaremos unha consulta preparada.
Por exemplo:
$stmt = $conex->prepare( 'UPDATE estados SET descripcion = :descripcion WHERE id = :id_estado' ); $stmt->execute([ ':descripcion' => $descripcion, ':id_estado' => $id ]);
Non construiremos a orde SQL mediante concatenación nin empregando sprintf() para introducir os datos.
Número de filas afectadas
Despois dun UPDATE podemos empregar:
$numRexistros = $stmt->rowCount();
para coñecer o número de filas afectadas.
No caso dunha modificación realizada a partir dunha clave primaria normalmente obteremos:
1: unha fila foi modificada.0: ningunha fila foi modificada.
IMPORTANTE: en MySQL un resultado de 0 non significa necesariamente que o identificador non exista.
Por exemplo, se a fila xa contén exactamente o mesmo valor que intentamos gardar, a orde pode executarse correctamente pero non producir ningún cambio.
Polo tanto, non debemos interpretar automaticamente:
$stmt->rowCount() === 0
como un erro.
Caso práctico: modificación dun estado
Imos continuar o mantemento da táboa ESTADOS realizado nas seccións anteriores.
O proceso será:
- Desde
consultar_estados.phpseleccionamos a opción Editar. - Enviamos o identificador do estado a
editar_estado.php. editar_estado.phpconsulta os datos actuais da fila.- Amosamos un formulario cos valores actuais.
- O formulario envía os novos datos mediante
POSTaprocesar_editar_estado.php. - Validamos novamente todos os datos no servidor.
- Executamos un
UPDATEmediante unha consulta preparada. - Rediriximos novamente a
consultar_estados.php.
- Desde
Neste caso é correcto empregar unha petición GET para abrir a páxina de edición:
editar_estado.php?id=5
porque esa petición unicamente consulta e amosa os datos.
A modificación real realizarase posteriormente mediante unha petición POST.
Arquivo /var/www/confBD/bd.php
Engadimos as seguintes consultas:
define( 'CONSULTAR_ESTADO_POR_ID', 'SELECT id, descripcion FROM estados WHERE id = :id_estado' ); define( 'ACTUALIZAR_ESTADO', 'UPDATE estados SET descripcion = :descripcion WHERE id = :id_estado' );
Fixarse en que tanto o identificador como a nova descrición se envían mediante parámetros.
Modificación de consultar_estados.php
Engadimos a opción Editar na columna de accións.
Podemos manter a consulta e a estrutura realizadas nas seccións anteriores:
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); $stmt = $conex->query(CONSULTAR_ESTADOS); $estados = $stmt->fetchAll(); } catch (PDOException $e) { error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); } ?> <!DOCTYPE html> <html lang="gl"> <head> <meta charset="UTF-8"> <title>Xestión de estados</title> </head> <body> <h1>Xestión de estados</h1> <form action="alta_estado.php" method="get"> <button type="submit">Novo estado</button> </form> <table> <thead> <tr> <th>Id</th> <th>Descrición</th> <th>Accións</th> </tr> </thead> <tbody> <?php foreach ($estados as $estado): ?> <tr> <td> <?= htmlspecialchars( (string) $estado['id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </td> <td> <?= htmlspecialchars( $estado['descripcion'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </td> <td> <a href="editar_estado.php?id=<?= urlencode( (string) $estado['id'] ) ?>"> Editar </a> <form action="procesar_borrar_estado.php" method="post" style="display:inline" onsubmit="return confirm( 'Estás seguro de que queres borrar este estado?' );" > <input type="hidden" name="id" value="<?= htmlspecialchars( (string) $estado['id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?>" > <button type="submit" name="btnBorrarEstado" value="BORRAR" > Borrar </button> </form> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php if (isset($_GET['msg'])): ?> <p> <?= htmlspecialchars( $_GET['msg'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </p> <?php endif; ?> <?php if (isset($_GET['error'])): ?> <p> <?= htmlspecialchars( $_GET['error'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </p> <?php endif; ?> </body> </html>
Aquí empregamos urlencode() para construír o valor do parámetro da URL.
Iso non substitúe a validación no servidor: editar_estado.php debe comprobar igualmente que o identificador recibido é válido.
Modificación de campos_estado.php
O arquivo campos_estado.php xa se utilizaba na operación de alta.
Modificámolo para que poida utilizarse tanto na alta como na edición:
<label for="descripcion">Descrición:</label> <input type="text" id="descripcion" name="descripcion" minlength="4" maxlength="100" required value="<?= htmlspecialchars( $descripcion ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?>" >
Se a variable $descripcion non existe, como sucederá inicialmente na páxina de alta, empregamos unha cadea baleira:
$descripcion ?? ''
Se estamos editando unha fila, $descripcion conterá o valor recuperado da base de datos.
IMPORTANTE: o dato gárdase na base de datos na súa forma normal. Aplicamos htmlspecialchars() neste momento porque estamos introducindo o valor dentro dun documento HTML.
Non debemos gardar previamente na base de datos o resultado de htmlspecialchars() ou htmlentities().
Arquivo editar_estado.php
Esta páxina recibe o identificador mediante GET, valida o seu formato, recupera os datos e amosa o formulario.
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); /* * Validamos o identificador recibido na URL. */ $id = filter_input( INPUT_GET, 'id', FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1 ] ] ); if ($id === false || $id === null) { header( 'Location: consultar_estados.php?error=' . urlencode('O identificador non é válido.') ); exit; } /* * Recuperamos os datos actuais do estado. */ $stmt = $conex->prepare( CONSULTAR_ESTADO_POR_ID ); $stmt->execute([ ':id_estado' => $id ]); $estado = $stmt->fetch(); if ($estado === false) { header( 'Location: consultar_estados.php?error=' . urlencode('Non se atopou o estado indicado.') ); exit; } /* * Estes valores aínda non se codifican para HTML. * O escapado farase no momento de xerar a saída. */ $id = (int) $estado['id']; $descripcion = $estado['descripcion']; } catch (PDOException $e) { error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); } ?> <!DOCTYPE html> <html lang="gl"> <head> <meta charset="UTF-8"> <title>Modificar estado</title> </head> <body> <h1>Modificar estado</h1> <form method="post" action="procesar_editar_estado.php" > <?php require 'campos_estado.php'; ?> <input type="hidden" name="id_orixinal" value="<?= htmlspecialchars( (string) $id, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?>" > <button type="submit" name="btnModificarEstado" value="MODIFICAR" > Modificar </button> </form> </body> </html>
Por que volvemos enviar o identificador?
A páxina editar_estado.php recibe o identificador mediante:
editar_estado.php?id=5
pero cando enviamos o formulario a:
procesar_editar_estado.php
necesitamos indicar que fila debe modificarse.
Por iso incluímos:
<input type="hidden" name="id_orixinal" value="5" >
IMPORTANTE: que un campo sexa hidden non significa que sexa seguro.
O usuario pode modificar manualmente o valor antes de enviar a petición. Por iso procesar_editar_estado.php debe validalo novamente.
Nunha aplicación con control de usuarios tamén deberemos comprobar que o usuario autenticado ten autorización para modificar a fila indicada.
Arquivo procesar_editar_estado.php
Esta páxina recibe os datos mediante POST, valida os valores e realiza o UPDATE.
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); /* * Comprobamos que se chegou desde * a operación prevista. */ if ( !isset($_POST['btnModificarEstado']) || $_POST['btnModificarEstado'] !== 'MODIFICAR' ) { header( 'Location: consultar_estados.php?error=' . urlencode('O identificador non é válido.') ); exit; /* Outra forma de enviar o erro throw new RuntimeException( 'Operación non válida.' ); */ } /* * Validamos o identificador. */ $id = filter_input( INPUT_POST, 'id_orixinal', FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1 ] ] ); if ($id === false || $id === null) { header( 'Location: consultar_estados.php?error=' . urlencode('O identificador non é válido.') ); exit; } /* * Recuperamos e normalizamos a descrición. */ $descripcion = trim( $_POST['descripcion'] ?? '' ); $lonxitude = mb_strlen( $descripcion, 'UTF-8' ); if ( $lonxitude < 4 || $lonxitude > 100 ) { header( 'Location: consultar_estados.php?error=' . urlencode( 'A descrición debe ter entre 4 e 100 caracteres.' ) ); exit; } /* * Normalización definida pola aplicación. */ $descripcion = mb_strtoupper( $descripcion, 'UTF-8' ); /* * Executamos o UPDATE mediante * unha consulta preparada. */ $stmt = $conex->prepare( ACTUALIZAR_ESTADO ); $stmt->execute([ ':descripcion' => $descripcion, ':id_estado' => $id ]); $numRexistros = $stmt->rowCount(); /* * En MySQL rowCount() pode devolver 0 se * o valor novo é igual ao que xa estaba gardado. */ if ($numRexistros === 1) { $mensaxe = 'Estado actualizado correctamente.'; } else { $mensaxe = 'A operación realizouse, pero non foi necesario ' . 'modificar ningún dato.'; } header( 'Location: consultar_estados.php?msg=' . urlencode($mensaxe) ); exit; } catch (PDOException $e) { error_log($e->getMessage()); header( 'Location: consultar_estados.php?error=' . urlencode('Erro relacionado coa base de datos.') ); exit; } catch (Throwable $e) { error_log($e->getMessage()); header( 'Location: consultar_estados.php?error=' . urlencode('Produciuse un erro na aplicación.') ); exit; }
Validación da descrición
Empregamos:
$descripcion = trim( $_POST['descripcion'] ?? '' );
para eliminar espazos ao principio e ao final.
Despois comprobamos a lonxitude con:
$lonxitude = mb_strlen( $descripcion, 'UTF-8' );
Empregamos mb_strlen() porque estamos traballando con texto UTF-8 e queremos contar correctamente caracteres como:
á é í ó ú ñ
A validación HTML realizada no formulario mediante:
minlength="4" maxlength="100" required
mellora a experiencia do usuario, pero non substitúe a validación no servidor.
Normalización a maiúsculas
Se a regra da aplicación indica que as descricións deben gardarse en maiúsculas, empregaremos:
$descripcion = mb_strtoupper( $descripcion, 'UTF-8' );
Por exemplo:
área pública
convértese en:
ÁREA PÚBLICA
Esta operación é unha normalización dos datos. Non é unha medida para evitar SQL Injection.
A protección fronte a SQL Injection obtense empregando correctamente a consulta preparada.
Comportamento de rowCount() nun UPDATE
Consideremos que a táboa contén:
id = 5 descripcion = ACTIVO
Se executamos:
UPDATE estados SET descripcion = 'INACTIVO' WHERE id = 5;
produciuse unha modificación e rowCount() normalmente devolverá:
1
Pero se executamos:
UPDATE estados SET descripcion = 'ACTIVO' WHERE id = 5;
cando o valor xa era ACTIVO, MySQL pode indicar:
0
filas modificadas.
Por iso un resultado de 0 non permite distinguir por si só entre:
- unha fila inexistente;
- unha fila existente cuxo valor xa era igual ao novo valor.
Se a aplicación necesita distinguir ambos os casos, podemos comprobar previamente a existencia da fila mediante unha consulta adicional ou deseñar a lóxica da aplicación para facelo.
Evitando valores duplicados
Se a descrición dun estado debe ser única, a regra debería estar garantida tamén na base de datos.
Por exemplo:
ALTER TABLE estados ADD CONSTRAINT uq_estados_descripcion UNIQUE (descripcion);
Deste xeito a base de datos impediría que unha modificación deixase dous estados coa mesma descrición.
Se se viola unha restrición UNIQUE, MySQL pode devolver un erro de clave duplicada.
PDO lanzará unha PDOException, que debe tratarse sen mostrar ao usuario directamente o contido de:
$e->getMessage()
O detalle técnico debe rexistrarse:
error_log($e->getMessage());
e ao usuario mostraráselle unha mensaxe axeitada.
Transaccións
- Se unha operación lóxica require varias operacións (alta/baixa/modificación) na base de datos e todas deben completarse ou cancelarse xuntas, debemos empregar unha transacción.
- Isto será explicado posteriormente nun punto da wiki.
Modificación mediante un procedemento almacenado
Tamén podemos realizar a modificación mediante un procedemento almacenado.
Por exemplo:
DELIMITER $$ CREATE PROCEDURE ACTUALIZAR_ESTADO_PROC( IN p_id INT, IN p_descripcion VARCHAR(100) ) BEGIN UPDATE estados SET descripcion = p_descripcion WHERE id = p_id; SELECT ROW_COUNT() AS rexistros_modificados; END$$ DELIMITER ;
No arquivo bd.php definimos:
define( 'PROCEDEMENTO_ACTUALIZAR_ESTADO', 'CALL ACTUALIZAR_ESTADO_PROC( :id_estado, :descripcion )' );
E desde PHP:
$stmt = $conex->prepare( PROCEDEMENTO_ACTUALIZAR_ESTADO ); $stmt->execute([ ':id_estado' => $id, ':descripcion' => $descripcion ]); $fila = $stmt->fetch(); $stmt->closeCursor(); $numRexistros = (int) $fila['rexistros_modificados'];
Neste caso:
- PDO non necesita
multi_query(). - Recuperamos o conxunto de resultados devolto polo procedemento.
- Chamamos a
closeCursor()para liberar posibles resultados pendentes antes de executar outra orde na mesma conexión.
- PDO non necesita
mysqli como ampliación
PDO será o mecanismo principal dos apuntes, pero podemos realizar a mesma operación empregando mysqli.
Conexión con mysqli
<?php try { require '/var/www/confBD/bd.php'; mysqli_report( MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT ); $conex = new mysqli( SERVIDOR, USUARIO, PASSWORD, BD ); $conex->set_charset('utf8mb4'); // Operacións coa base de datos... } catch (mysqli_sql_exception $e) { error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); }
Consultas preparadas con mysqli
No arquivo bd.php poderiamos definir:
define( 'CONSULTAR_ESTADO_POR_ID_MYSQLI', 'SELECT id, descripcion FROM estados WHERE id = ?' ); define( 'ACTUALIZAR_ESTADO_MYSQLI', 'UPDATE estados SET descripcion = ? WHERE id = ?' );
Para consultar unha fila:
$stmt = $conex->prepare( CONSULTAR_ESTADO_POR_ID_MYSQLI ); $stmt->bind_param( 'i', $id ); $stmt->execute(); $resultado = $stmt->get_result(); $estado = $resultado->fetch_assoc();
Para modificar:
$stmt = $conex->prepare( ACTUALIZAR_ESTADO_MYSQLI ); $stmt->bind_param( 'si', $descripcion, $id ); $stmt->execute(); $numRexistros = $stmt->affected_rows;
Neste caso:
sindica unha cadea.iindica un enteiro.
Non necesitamos empregar:
$conex->real_escape_string(...)
nin construír a consulta con sprintf(), porque os valores se envían mediante unha consulta preparada.
Resumo
- En PHP 8.5 empregaremos PDO como mecanismo principal para realizar modificacións.
- As consultas gardaranse en
/var/www/confBD/bd.php, fóra de/var/www/html. - Os valores variables dun
UPDATEenviaranse mediante consultas preparadas. - Non construiremos a orde SQL concatenando ou interpolando datos recibidos do usuario.
- Empregar
GETpara abrir o formulario de edición é correcto porque esa petición non modifica datos. - A modificación realizarase mediante
POST. - Un formulario
POSTnon substitúe a protección CSRF. - Os identificadores recibidos deben validarse no servidor.
- Un campo
hiddenpode ser modificado polo usuario. - A validación dun identificador non substitúe as comprobacións de autorización.
- Os datos recuperados da base de datos non se codifican antes de gardalos nunha variable.
- Empregaremos
htmlspecialchars()cando introduzamos os datos nun documento HTML. - Para texto UTF-8 empregaremos
mb_strlen()e, cando sexa necesario normalizar a maiúsculas,mb_strtoupper(). PDOStatement::rowCount()permite coñecer cantas filas foron modificadas por unUPDATE.- En MySQL,
rowCount() === 0non implica necesariamente que a fila non exista: pode significar que o novo valor era igual ao xa gardado. - As regras de unicidade e integridade deben estar garantidas tamén pola base de datos.
- Os detalles técnicos das excepcións deben gardarse no log e non mostrarse directamente aos usuarios.
mysqliexplicarase como alternativa adicional, tamén empregando consultas preparadas.
-- Ángel D. Fernández González -- (2026).