PHP UD3 TRANSACCIONS

De MediaWiki
Revisión del 18:55 25 oct 2023 de Angelfg (discusión | contribuciones) (Página creada con «==Introducción== * En certas ocasións imos necesitar facer varias operacións que conleven modificacións nas táboas da base de datos (operacións de INSERT, UPDATE, DE…»)
(dif) ← Revisión anterior | Revisión actual (dif) | Revisión siguiente → (dif)
Ir a la navegación Ir a la búsqueda

Introducción

  • En certas ocasións imos necesitar facer varias operacións que conleven modificacións nas táboas da base de datos (operacións de INSERT, UPDATE, DELETE). Se queremos que nunha páxina php se fagan varias modificacións e queremos que se hai calquera problema nunha delas, deixe o estado da base de datos xusto a como estaba antes de facer a primeira operación, necesitamos facer uso das transaccións.



  • Por exemplo, no exemplo que estamos a desenvolver:
  • Podemos crear unha páxina PHP cun formulario para dar de alta un libro xunto cos exemplares.
  • Podemos crear unha páxina PHP que relacione libros xunto cos autores que o escribiron.
  • Podemos dar de baixa un libro. Isto leva consigo dar de baixa o libro máis os exemplares máis os datos da táboas LIBROS_AUTORES.
  • Outro caso típico é o carrito da compra. Cando o usuario remata o proceso de compra e procede a realizar o pedido, necesitaremos gardar os produtos que conforman o pedido na nosa base de datos.
Queremos que se garden todos os produtos.


Normalmente imos queres que ditas operacións se fagan todas e en caso de erro, necesitaremos volver ao estado anterior ao comezo do inicio das operacións.
  • Isto o podemos facer grazas as TRANSACCIÓNS.
Unha transacción nos garante que o conxunto de operacións que están dentro dunha transacción se farán todas ou non se fará ningunha.
  • Normalmente cada operación individual é 'considerada' coma unha transacción en si mesma, de tal forma o ou ten éxito ou os cambios non se gardan na base de datos.
  • Máis información sobre Transaccións:



  • Cando falamos de transaccións outro factor a ter en conta é o chamado Nivel de illamento.
Imaxinemos que estamos dentro dunha transacción e estou lendo o número de filas dunha táboa. Se outro usuario engade unha nova fila despois de que eu lera o número, e eu dentro da transacción volvo a ler o número de filas. Terei dúas cantidades diferentes dentro da mesma transacción.
Para evitalo podemos facer que a táboa que estou a utilizar dentro da transacción quede 'bloqueada' e non se poida modificar ata que eu remate.
Este e outros problemas que poden darse no uso de transaccións se intentan evitar cun nivel de illamento determinado.
Cada nivel de illamento soluciona algún dos problemas que se poden dar.
  • Máis información sobre Niveis de illamento en Mysql:



  • Dos niveis de illamento (que se basean en bloquear rexistros de táboas) se pode provocar outro problema, que é o Interbloqueo ou Deadlock.
Isto prodúcese cando unha transacción_1 está esperando o acceso a un recurso que está sendo bloqueado por outra transacción_2 e esta outra transacción_2 está esperando por un recurso que está sendo bloqueado pola transacción_1.




  • Nota importante: Lembrar que xa expliquei ao abrir a conexión, no control de erros así como tamén nas operacións de consulta/alta/baixa/modificar que se estamos a empregar á versión PHP 8.1 ou superior, vanse lanzar excepcións que teremos que teremos que capturar con catch.



Funcionamento

  • En PHP, para poder utilizar transaccións necesitamos:


  • Unha transacción a podemos facer dende a páxina PHP ou dende o xestor Mysql dentro dun procedemento almacenado.
  • Para realizar unha transacción o proceso sempre é o mesmo en todas as linguaxes e xestores.
  1. Iniciar a transacción. Normalmente farase coa orde 'START TRANSACTION' (outros xestores teñen coma orde BEGIN TRANSACTION)
  2. Realizar as operacións que conforman as transaccións.
  3. En caso de erro en algunha das operacións anteriores executar a orde 'ROLLBACK' e saír
  4. En caso de que todo se execute correctamente, entón confirmamos a transacción e os cambios se gardan definitivamente na base de datos, coa orde: 'COMMIT'



  • Para iniciar unha transacción temos dúas opcións:
  • Versión procedimental:
1 $conex = mysqli_connect(HOST, USER, PASSWORD, BD);
2 mysqli_autocommit($conex,false);
  • Versión obxectual:
1 $conex= new mysqli(HOST, USER, PASSWORD, BD);
2 $conex->autocommit(false);
Desta forma as operacións que vaian a continuación desta orde non se farán efectivas ata atopar un commit.
NECESITAMOS QUE A VERSIÓN PHP SEXA 5.5 OU SUPERIOR.
  • Versión procedimental:
1 $conex = mysqli_connect(HOST, USER, PASSWORD, BD);
2 mysqli_begin_transaction($conex,MYSQLI_TRANS_START_READ_ONLY);
  • Versión obxectual:
1 $conex= new mysqli(HOST, USER, PASSWORD, BD);
2 $conex->begin_transaction(MYSQLI_TRANS_START_READ_ONLY);


Esta segunda forma leva como parámetro unha bandeira que vai indicar que dentro da transacción non vai poderse facer operacións que modifiquen os contidos das táboas da base de datos.
Se a transacción necesita facer operacións contra a base de datos, levaría o flag MYSQLI_TRANS_START_READ_WRITE.
Máis información neste enlace.


  • Unha vez iniciada a transacción faremos as operacións necesarias sobre a base de datos. En caso de erro nalgunha delas, teremos que facer un rollback coa función rollback.
  • Versión procedimental:
1 $conex = mysqli_connect(HOST, USER, PASSWORD, BD);
2 mysqli_begin_transaction($conex,MYSQLI_TRANS_START_READ_ONLY);
3 
4 .........
5 OPERACIÓN
6 Si erro
7    mysqli_rollback($conex);
8    die('Houbo un erro...');
  • Versión obxectual:
1 $conex= new mysqli(HOST, USER, PASSWORD, BD);
2 $conex->begin_transaction(MYSQLI_TRANS_START_READ_ONLY);
3 
4 .........
5 OPERACIÓN
6 Si erro
7    $conex->rollback();
8    die('Houbo un erro...');


  • Exemplo (obxectual) empregando unha versión de PHP 8.1 onde as ordes contra a base de datos poden provocar unha excepción:
 1 try {
 2     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,DB);   // Constantes definidas nun arquivo fora do sitio web
 3     $conex->set_charset('utf8mb4');
 4     
 5     $conex->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
 6     $result=$conex->query("INSERT INTO TABLE_1 (col2) VALUES ('valor')");
 7     if ($result){
 8        // Operación correcta
 9        printf ("Novo rexistro có id %d.\n", $conex->insert_id);
10     }
11     $result=$conex->query("INSERT INTO TABLE_2 (col2) VALUES ('valor')");
12     if ($result){
13        // Operación correcta
14        printf ("Novo rexistro có id %d.\n", $conex->insert_id);
15     }
16 
17 }   // Pecha o try da parte superior da páxina
18 catch (mysqli_sql_exception $e) {
19     if(!empty($conex)) $conex->rollback();
20     die("Erro mysql: " . $e->getMessage());
21 }
22 catch (Expcetion $e) {
23     if(!empty($conex)) $conex->rollback();
24     die("Erro xeral: " . $e->getMessage());
25 }
26 finally{
27   if(!empty($conex)){
28       $conex->close();
29   }
30 }


  • Versión procedimental:
1 mysqli_commit($conex);
  • Versión obxectual:
1 $conex->commit();



  • Se establecimos autocommit(false) deberemos volvelo a establecer en True.
1 $conex->autocommit(true);


  • Ao rematar todas as operacións sobre a base de datos pecharemos a conexión.
1 $conex->close();



O exemplo anterior de forma completa (empregando unha versión de PHP 8.1 onde as ordes contra a base de datos poden provocar unha excepción:):

 1 try {
 2     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,DB);   // Constantes definidas nun arquivo fora do sitio web
 3     $conex->set_charset('utf8mb4');
 4     
 5     $conex->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
 6     $result=$conex->query("INSERT INTO TABLE_1 (col2) VALUES ('valor')");
 7     if ($result){
 8        // Operación correcta
 9        printf ("Novo rexistro có id %d.\n", $conex->insert_id);
10     }
11     $result=$conex->query("INSERT INTO TABLE_2 (col2) VALUES ('valor')");
12     if ($result){
13        // Operación correcta
14        printf ("Novo rexistro có id %d.\n", $conex->insert_id);
15     }
16     $conex->commit();
17 
18 }   // Pecha o try da parte superior da páxina
19 catch (mysqli_sql_exception $e) {
20     if(!empty($conex)) $conex->rollback();
21     printf("Erro mysql: " . $e->getMessage());  // Se non poñemos die executa o finally. Se non habería que poñer o autocommit aquí tamén
22 
23 }
24 catch (Expcetion $e) {
25     if(!empty($conex)) $conex->rollback();
26     printf("Erro xeral: " . $e->getMessage());  // Se non poñemos die executa o finally. Se non habería que poñer o autocommit aquí tamén
27 }
28 finally{
29   if(!empty($conex)){
30       $conex->close();
31   }
32   $conex->autocommit(true);
33 
34 }




Transaccións en MYSQL dentro de procedementos almacenados

  • Outra forma de realizar as transaccións é que poidamos enviar os datos a un procedemento almacenado de Mysql e que dende o procedemento se faga a transacción.
  • O proceso é moi parecido ao feito dende PHP.
Lembrar que se o facemos dende o propio procedemento NON TEMOS QUE FACELO dende PHP.
 1 DELIMITER $$
 2 
 3 CREATE PROCEDURE `PROCEDEMENTO_CON_TRANSACCION`(_param1 int,_param2 varchar(50),_param3 varchar(200))
 4 BEGIN
 5 	DECLARE EXIT HANDLER FOR SQLEXCEPTION
 6 	BEGIN
 7 		ROLLBACK;
 8 		SELECT -1 as error;
 9 	END;
10 
11 	START TRANSACTION;
12 
13 	INSERT INTO TABLE_1(col1,col2)
14 	VALUES (_param2,_param3);  -- Supoñemos que esta táboa devolve un valor clave autonumérico.
15 
16 	INSERT INTO TABLE_2(col1,col2)
17 	VALUES (_param1,last_insert_id());
18 
19 	
20 	COMMIT;
21 	SELECT 0 AS error;
22 
23 END
24 $$
  • Liñas 5-9: Capturamos calquera erro que se produza e nese caso enviamos a PHP un código -1 no select facendo un rollback da transacción.
  • Liña 11: Iniciamos a transacción.
  • Liñas 20 e 21: Confirmamos a transacción se todo foi ben e enviamos un 0 como código ao PHP.


  • Comentar que se facemos as transaccións dende procedementos almacenados teremos que capturar as posibles excepcións que se produzan para facer o rollback.
Máis información: Captura de erros en Mysql.




  • Por exemplo, no caso da base de datos que estamos a desenvolver nesta sección, poderíamos crear un procedemento almacenado que dea de alta a un único autor xunto có libro.
Isto leva consigo que o procedemento vai ter que dar de alta datos nas táboas: libros, autores e libro_autor.
 1 DELIMITER $$
 2 
 3 CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_AUTORES_LIBRO`(p_titulo varchar(300),p_nome varchar(50),p_apelidos varchar(200))
 4 BEGIN
 5 	DECLARE v_id_libro int;
 6 
 7 	DECLARE EXIT HANDLER FOR SQLEXCEPTION
 8 	BEGIN
 9 		ROLLBACK;
10 		SELECT -1 as error; 
11 	END;
12 
13 	START TRANSACTION;
14 	INSERT INTO libros(titulo)
15     VALUES (p_titulo);
16     
17     SET v_id_libro = last_insert_id();
18       
19 
20 	INSERT INTO autores(nome,apelidos)
21 	VALUES (p_nome,p_apelido);
22 
23 	INSERT INTO LIBROS_AUTORES (libro_id,autor_id)
24 	VALUES (v_id_libro,last_insert_id());
25 	
26 	COMMIT;
27 	SELECT 0 AS error;
28 
29 END$$
30 DELIMITER ;




Exercicio proposto 1

  • Asociar libros a autores.
  • A idea é escoller dunha lista (<select>) un libro.
Unha vez seleccionado un libro e premido o botón BUSCAR aparecerán nunha lista de checkboxes todos os autores. O libro debe manterse seleccionado ao amosar todos os autores.
  • O usuario poderá checkear varios autores e serán asociados a dito libro.
Os autores xa asociados ao libro terán que estar chequeados.
  • Ao premer o botón de ALTA, se asociarán ao libro seleccionado.
Isto levará consigo as operacións de baixa de todos os autores do libro e a de alta sobre a táboa LIBROS_AUTORES por cada un dos autores seleccionados.


Pista: Lembrar que en html, se damos de nome a un input có seguinte formato: dato[] e repetimos este nome en varios input, cando enviemos o dato, en PHP o poderemos recoller en formato de array.
  • Para separar a lóxico, crea as seguintes páxinas:
  • alta_libro_autores.php => Amosa o libro e os autores
  • procesar_alta_libro_autores.php => Procesa a alta e volve a amosar alta_libroautores.php




  • POSIBLE SOLUCIÓN (algunha das constantes xa foron creados en solucións anteriores)


Arquivo: bd.inc.php

define ('SERVIDOR','localhost');
define ('USUARIO','user_php');
define ('PASSWORD','user_php');
define ('BD','php');


define('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
define('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
define('CONSULTAR_AUTORES_POR_LIBRO','SELECT id,apelidos,nome FROM autores INNER JOIN libro_autor WHERE libro_id=%d order by apelidos,nome');
define('INSERIR_LIBRO_AUTOR' 'INSERT INTO libro_autor (libro_id, autor_id) VALUES (%d,%d)');
define('BORRAR_LIBRO_AUTOR_POR_LIBRO','DELETE FROM libro_autor WHERE libro_id=%d');


Arquivo: alta_libro_autores.php

<?php
/* Lembrar quitar isto ao probar o scripts sen erros */
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

require ('/var/www/confBD/bd.inc.php');

try {
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);   
    $conex->set_charset('utf8mb4');
    $resultado = $conex->query(CONSULTAR_LIBROS);

    $listaLibros = [0 => ['id' => -1, 'titulo' => 'Escolle un libro']];
    while($fila = $resultado->fetch_assoc()) {
        $listaLibros[] = $fila;
    }
    $resultado->free();

?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Xestión de libros-autores</title>
    </head>
    <body>

        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method="post">
            <select name='lstLibros'>
                <?php 
                    $libroSeleccionado=-1;
                    if(!empty($_POST['btnLibro'])) $libroSeleccionado = $_POST['lstLibros'];    // Vimos de seleccionar un libro

                    foreach ($listaLibros as $libro) {
                        $cadena = '';   // Para facer que o libro se manteña seleccionado ao premer no botón
                        if($libroSeleccionado == $libro['id']) $cadena = "selected='selected'";
                        
                        printf("<option value=%d %s>%s</option>",htmlentities($libro['id']),$cadena,htmlentities($libro['titulo']));
                    }
                ?>
            </select>
            <input type='submit' value='BUSCA AUTORES' name='btnLibro' />
        </form>

        <?php 
            if(!empty($_POST['btnLibro']))  {           // Vimos de seleccionar un libro
                $listaAutoresLibro = [];
                $consulta = sprintf(CONSULTAR_AUTORES_POR_LIBRO,htmlentities($_POST['lstLibros']));
                $resultado = $conex->query($consulta);
                while($fila = $resultado->fetch_assoc()) {
                    $listaAutoresLibro[] = $fila['id'];
                }
                $resultado->free();
            
                $resultado = $conex->query(CONSULTAR_AUTORES);
                while($fila = $resultado->fetch_assoc()) {
                    $listaAutores[] = $fila;
                }
                $resultado->free();
        ?>

        <form action="procesar_alta_libro_autores.php" method="post">
            <table border="1" style="margin-top:20px">
                <tr>
                    <th>Autores</th>
                    <th></th>
                </tr>
                <?php 
                printf("<input type='hidden' name='libro_id' value=%d",htmlentities($_POST['lstLibros']));  // Necesito enviar o id do libro xunto cos id's dos autores
                foreach($listaAutores as $autor){
                    $cadena = '';
                    if (in_array($autor['id'],$listaAutoresLibro)){ // O autor xa está asociado ao libro
                        $cadena = "checked='checked'";
                    }
                    printf("<tr><td>%s</td>",htmlentities($autor['apelidos'] . ',' . $autor['nome']));
                    printf("<td><input name=autores[] type='checkbox' value=%d %s /></td></tr>",htmlentities($autor['id']), $cadena);
                }

                ?>
            </table>
            <div style='margin-top:30px'><input type='submit' value='ASOCIAR' name='btnAsociar' /></div>
        </form>


        <?php 
            }   // Fin do if seleccionar un libro
        ?>


        <div>
            <?php
                if(isset($_GET['id'])) printf("<h3>Estado con id %d dado de alta correctamente.</h3>",htmlentities($_GET['id']));
                if(isset($_GET['error'])) printf("<h2>Houbo un erro: %s</h2>",htmlentities($_GET['error']));
            ?>
        </div>    
        
    </body>    
</html>
<?php
}   // Pecha o try da parte superior da páxina
catch (mysqli_sql_exception $e) {
    die("Erro mysql: " . $e->getMessage());
}
catch (Expcetion $e) {
    die("Erro xeral: " . $e->getMessage());
}
finally{
  if(!empty($conex)){
      $conex->close();
  }
}
?>


Arquivo: procesar_alta_libro_autores.php

<?php
/* Lembrar quitar isto ao probar o scripts sen erros */
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

require ('/var/www/confBD/bd.inc.php');

$error="";   // Variable que a usaremos para amosar os posibles erros que poidan darse
$msg='';    // Variable empregada para enviar o novo id xerado 
try {
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);   
    $conex->set_charset('utf8mb4');
    $conex->autocommit(false);
    // Xestionamos a operacion de asociar
    if (!empty($_POST['btnAsociar'])){  
        $libro_id = $conex->real_escape_string($_POST['libro_id']);
        $query = sprintf(BORRAR_LIBRO_AUTOR_POR_LIBRO,$libro_id);   // Borra todos los autores do libro dado
        $conex->query($query);

        foreach($_POST['autores'] as $autor){
            $query = sprintf(INSERIR_LIBRO_AUTOR,$libro_id,$conex->real_escape_string($autor));
            $conex->query($query);
        }
    }
    $conex->commit();
}   // Pecha o try da parte superior da páxina. Lembrar que aos usuarios non se lle deben amosar os erros. Isto está posto a nivel informativo para os programadores
catch (mysqli_sql_exception $e) {
    if(!empty($conex)) $conex->rollback();
    $error=sprintf("error='Erro mysql: %s'",htmlentities($e->getMessage()));
}
catch (Expcetion $e) {
    if(!empty($conex)) $conex->rollback();
    $error=sprintf("error='Erro xeral: %s'",htmlentities($e->getMessage()));
}
finally{
    if(!empty($conex)){
        $conex->autocommit(true);
        $conex->close();
    }
}

$url = sprintf("alta_libro_autores.php?%s",$error);
header("Location:" . $url);

?>




Exercicio proposto 2: Transaccións en MYSQL dentro de procedementos almacenados

  • Imos crear unha páxina de nome alta_libro_exemplares.php.
Dita páxina terá un formulario onde indicaremos o título do libro e o número de exemplares que queremos dar de alta.
Ao premer o botón de 'Alta' aparecerán en forma de lista (o podedes implementar cunha táboa) un formulario (unha por cada exemplar) cos datos: imaxe-prezo-estado.
Os datos anteriores (título e número de exemplares) non se poderán cambiar.
  • Os campos do formulario serán dos seguintes tipo:
  • Estado: Será un combobox (select en html) cos estados da táboa estados.
  • Imaxe: cambiaremos isto máis adiante. Por agora deixade un campo de texto.
  • Prezo: Prezo da imaxe.
  • Como sempre deberedes validar os datos e que teñan o tipo/formato correcto.
  • No segundo formulario aparecerá un botón de 'Alta' e outro de 'Cancelar'.
  • Ao premer o botón de alta daremos de alta o libro, obteremos o id autoxerado e usaremos ese id para dar de alta aos exemplares.
  • Se prememos o botón 'Cancelar' desaparecerá o segundo formulario e limparase o primeiro para que poidamos dar de alta un novo libro.



  • POSIBLE SOLUCIÓN (algúns dos procedementos xa foron creados en solucións anteriores)

MYSQL

Procedemento: CONSULTAR_ESTADOS

DELIMITER $$
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_ESTADOS`()
BEGIN
	SELECT id,descripcion
	FROM estados
	ORDER BY descripcion;

END$$
DELIMITER ;



Procedemento: INSERIR_LIBRO

DELIMITER $$

CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_LIBRO`(p_titulo varchar(300))
etiqueta:BEGIN  -- Asociamos unha etiqueta ao código do procedemento para que poidamos facer algo parecido a un exit
    declare existe int;
       
	SELECT count(*)
	INTO existe
	FROM libros
	WHERE upper(titulo)=upper(p_titulo);
 
	IF existe=1 THEN
			begin
			   SELECT -1 as valor_devolto;
			   leave etiqueta; -- Para que saia do procedemento
			end;
        END IF;  -- Fin do IF

	INSERT INTO libros (titulo)
	VALUES (p_titulo);

	SELECT last_insert_id() as valor_devolto;
END$$
DELIMITER ;



Procedemento: INSERIR_EXEMPLAR

DELIMITER $$
CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_EXEMPLAR`(p_libro_id int,p_id_exemplar int,p_imaxe varchar(100),p_prezo decimal(6,2),p_estado_id int)
BEGIN
	INSERT INTO exemplares (libro_id,id_exemplar,imaxe,prezo,estado_id)
	VALUES (p_libro_id,p_id_exemplar,p_imaxe,p_prezo,p_estado_id);

END$$
DELIMITER ;



Arquivo bd.inc.php:

1 define('PROCEDEMENTO_INSERIR_LIBRO',"INSERIR_LIBRO('%s')");
2 define('PROCEDEMENTO_INSERIR_EXEMPLAR',"INSERIR_EXEMPLAR(%d,%d,'%s',%f,%d)");
3 define('PROCEDEMENTO_CONSULTAR_ESTADOS','CONSULTAR_ESTADOS()');




Arquivo alta_libro_exemplares.php

<?php
/* Lembrar quitar isto ao probar o scripts sen erros */
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

require ('/var/www/confBD/bd.inc.php');

$error="";   // Variable que a usaremos para amosar os posibles erros que poidan darse
$msg='';    // Variable empregada para enviar o novo id xerado 

// *********************************ALTA ************************************************************************//
if (!empty($_POST['btnAlta'])){ // DAMOS DE ALTA A EXAMPLARES E O LIBRO
    foreach ($_POST['lstEstado'] as $indice => $estado){    // Usamos o índice para buscar os datos no resto dos campos => Fixarse que é nu foreach
        $prezo = $_POST['txtPrezo'][$indice];
        $imaxe = $_POST['txtImaxe'][$indice];

        // Xa temos os datos de cada un dos exemplares
        // Verifacamos que cumpran os criterios de tipo e formato => PRIMEIRO VALIDAMOS. SE SON CORRECTOS TODOS INICIAMOS A OPERACION DE ALTA
        if (!filter_var($estado,FILTER_VALIDATE_INT) || $estado<0){
            $error .= "O estado é incorrecto...";
            break;
        }
        if (filter_var(str_replace(',', '.', $prezo),FILTER_VALIDATE_FLOAT)==false || $prezo<=0 || !preg_match("/[0-9]{1,6}(,[0-9]{1,2})?/",$prezo)){
            $error .= "O prezo é incorrecto...";
            break;
        }
        $imaxe=filter_var($imaxe,FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
        if(((strlen($imaxe))>100) || (strlen($imaxe)==0)) {    // O número de caracteres está establecido na táboa da base de datos
            $error .= "O nome da imaxe é demasiado grande ou non foi posto...";
            break;
        }
        // Gardamos os datos validados para dalos de alta todos xuntos
        $dato['prezo'] = $prezo;
        $dato['imaxe'] = $imaxe;
        $dato['estado'] = $estado;
        $datos[] = $dato;
    }   // Fin do foreach
    
    
    if(empty($error)){  // Non houbo error, empezamos a operación de alta con transaccións
        try{

            $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);   
            $conex->set_charset('utf8mb4');
            
            $conex->autocommit(false);  // Iniciamos a transcción
            
            // Damos de alta ao libro e obtemos o id xerado
            $titulo = $conex->real_escape_string($_POST['txtTitulo']);
            $proc=sprintf(PROCEDEMENTO_INSERIR_LIBRO,$titulo);
            $proc = sprintf("CALL %s",$proc);
            if ($conex->multi_query($proc)){
                $result = $conex->store_result();
                if ($result && ($result->num_rows>0)){
                    $fila=$result->fetch_assoc();
                    switch($fila['valor_devolto']){
                        case -1:
                            throw new Exception("Ese libro xa est&aacute; dado de alta na base de datos...<br />");
                        default:
                            $id_libro = $fila['valor_devolto'];    // XA TEMOS O ID AUTONUMERICO DO LIBRO
                    }
                    $result->free();
                    while($conex->next_result() && $conex->store_result());
                }
                else{   // Sempre ten que devolver algo como temos implementado o proc.
                    throw new Exception('Houbo un erro na chamada á base de datos<br />');
                }
            }
            
            
            // Temos que dar de alta aos exemplares 
            $cont_num_exemplar=1;   // Número de exemplar
            foreach ($datos as $dato){
                $prezo = $conex->real_escape_string($dato['prezo']);
                $estado = $conex->real_escape_string($dato['estado']);
                $imaxe = $conex->real_escape_string($dato['imaxe']);
                $proc=sprintf(PROCEDEMENTO_INSERIR_EXEMPLAR,$id_libro,$cont_num_exemplar,$imaxe,$prezo,$estado);
                $proc = sprintf("CALL %s",$proc);
                if ($conex->multi_query($proc)){
                    $result = $conex->store_result();   // Neste caso o procedemento non devolve nada => vale 'false'
                    if ($conex->errno>0){
                        $error .= 'Houbo un erro na alta dos exemplares';
                        break;  //Saimos do for
                    }
                    while($conex->next_result() && $conex->store_result());
                }                   
                $cont_num_exemplar+=1;
            }   // Fin do For por cada exemplar

            $conex->commit();
    
        }   // Pecha o try da parte superior da páxina. Lembrar que aos usuarios non se lle deben amosar os erros. Isto está posto a nivel informativo para os programadores
        catch (mysqli_sql_exception $e) {
            if(!empt($conex)) $conex->rollback();
            $error=sprintf("error='Erro mysql: %s'",htmlentities($e->getMessage())); 
        }
        catch (Expcetion $e) {
            if(!empt($conex)) $conex->rollback();
            $error=sprintf("error='Erro xeral: %s'",htmlentities($e->getMessage()));
        }
        finally{
            if(!empty($conex)){
                $conex->close();
            }
        }            
        
    }   // Fin de (empty($error)
}

// *********************************FIN DE ALTA ************************************************************************//
?>


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Transaccións</title>
    </head>

    <body>
        <?php
            $titulo='';
            $num_exemplares='';
            if (!empty($_POST['btnEnviar'])) {  // Prememos o botón que indica o título e o número de exemplares
                $titulo = filter_var($_POST['txtTitulo'],FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
                if (strlen($titulo)>200){   // Non pode ter unha lonxitude maior que o campo da base de datos
                    $error='A lonxitude do título non pode ser maior que 200 caracteres...<br />';
                }
                $num_exemplares = $_POST['txtNumExemplares'];
                if (!filter_var($num_exemplares,FILTER_VALIDATE_INT) || $num_exemplares > 9 || $num_exemplares < 1){
                    $error.="O número de exemplares ten que ser numérico e non pode ser maior que 9<br />";
                }
                if (strlen($titulo)==0) {   
                    $error.='O título é obrigatorio...<br />';
                }
            }
        ?>
        
        <?php
            $inhabilitar='';
            if (!empty($_POST['btnEnviar']) && (empty($error))) {   // Facemos que a caixa de texto e todo e demais quede inhabilitado 
                $inhabilitar="disabled='disabled'";                  // Unha vez enviamos os datos do titulo e num exemplares
            }
        ?>
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
            <div>Titulo Libro: <input <?php echo $inhabilitar ?> <?php printf(" value='%s'",$titulo); ?> type='text' size='100' maxlength="300" name='txtTitulo' /></div>
            <div>Num. Exemplares: <input <?php echo $inhabilitar ?>  <?php printf(" value='%s'",$num_exemplares); ?> type='number' min='1' max='9' size="2" maxlength="1" name='txtNumExemplares' /> (&lt;10) </div>
            <input <?php echo $inhabilitar ?> type="submit" value='Enviar' name='btnEnviar' />    
        </form>

    
        
        <?php 
        if ($titulo && $num_exemplares){    // Enviamos datos
            // NON IMOS A FACER UNHA CONSULTA A TABOA ESTADOS POR CADA UN DOS EXEMPLARES. NON TEN SENTIDO.
            // PODERIAMOS PERCORRER O ARRAY DE RESULTADOS E AO REMATAR VOLVER AO PRINCIPIO: http://php.net/manual/es/mysqli-result.data-seek.php

            try {
                $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);   
                $conex->set_charset('utf8mb4');
            
                $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_ESTADOS);
                if ($conex->multi_query($proc)){
                    $result = $conex->store_result();
                    if ($result && ($result->num_rows>0)){
                        while($valor = $result->fetch_assoc()){
                            $estados[]=$valor;
                        }
                        $result->free();
                        while($conex->next_result() && $conex->store_result());
                    }
                }
            }   // Pecha o try da parte superior da páxina. Lembrar que aos usuarios non se lle deben amosar os erros. Isto está posto a nivel informativo para os programadores
            catch (mysqli_sql_exception $e) {
                $error.=sprintf("Erro mysql: %s",htmlentities($e->getMessage()));
            }
            catch (Expcetion $e) {
                $error.=sprintf("Erro xeral: %s",htmlentities($e->getMessage()));
            }
            finally{
                if(!empty($conex)){
                    $conex->close();
                }
            }

            printf("<form action=\"%s\" method='post'>",htmlentities($_SERVER['PHP_SELF']));
            printf("<input type='hidden' value='%s' name='txtTitulo' />",htmlentities($_POST['txtTitulo'])); // Como temos dous formularios, temos que gardar o texto do título nun campo oculto
            printf ("<table>");
            for($cont=0;$cont<$num_exemplares;$cont++){
                echo "<th>Estado</th><th>Imaxe</th><th>Prezo</th>";
                echo "<tr>";
                echo "<td>";    // ESTADO
                    printf ("<select name='lstEstado[]>");  // Poñemos o nome en forma de array xa que imos recibir varios exemplares con datos.
                    foreach ($estados as $estado){
                        printf("<option value='%d'>%s</option>",htmlentities($estado['id']),htmlentities($estado['descripcion']));
                    }
                    printf ("</select>");
                echo "</td>";
                echo "<td>";    // IMAXE
                    printf("<input type='text' size='50' maxlength='70' name='txtImaxe[]' />");
                echo "</td>";
                echo "<td>";    // PREZO
                    printf("<input type='text' size='9' maxlength='9' name='txtPrezo[]' /> Euros (XXXXXX,YY)");
                echo "</td>";
                echo "</tr>";
            }
            
            printf ("</table>");
            printf("<input type='submit' value='ALTA' name='btnAlta' />");    
            printf("<input type='reset' value='CANCELAR' name='btnCancelar' />");    
            printf("</form>");
            
        }
        ?>
        <?php
            if (!empty($error)) {   // En caso de atopar erros os amosa 
                printf("<h4>$error</h4>");
            }
        ?>

    </body>    
</html>





  • Exercicio proposto
Tedes neste enlace da WIKI a forma de subir unha imaxe ao servidor. Modificade o código anterior para facelo.


  • Exercicio proposto
Amplía a funcionalidade da páxina anterior para poder asociar a os autores cos libros na mesma operación de alta.
Para iso fai que os autores se carguen nunha lista (<select con size en HTML>) ordenados alfabeticamente e que se poidan seleccionar varios.
En caso de que os autores non estean dados de alta, poderás premer nun enlace que abrirá unha xanela nova (sen o toolbar, botóns,...) e que permitirá dar de alta a un novo autor. Ao pechar a xanela deberán refrescarse os datos da lista para amosar o novo autor, mantendo os datos previamente introducidos.







-- Ángel D. Fernández González -- (2023).