Diferencia entre revisiones de «PHP UD3 OP ALTA»

De MediaWiki
Ir a la navegación Ir a la búsqueda
Línea 1: Línea 1:
−
===Introducción===
+
=== Introdución ===
  
 +
Nesta unidade veremos como realizar operacións de alta sobre unha base de datos MySQL/MariaDB desde PHP 8.5.
  
−
* '''<u>Requerimentos previos:</u>'''  
+
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.
−
:* 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 [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro como foi indicado nunha sección anterior]
 
−
:* Estes exercicios están baseados nunha versión de PHP 8.1 ou superior onde as ordes PHP para manexar a base de datos lanzan expcecións. Se tedes unha vesión anterior, teredes que facer o indicado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_CONSULTA na sección de consultas] e na [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Control_de_errores sección de conexión].
 
  
 +
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'''.
  
−
* '''NOTA IMPORTANTE:''' [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios Vimos en seccións anteriores] que cando empregmos datos dun formulario, existen unhas técnicas para evitar posibles ataques. Todos os exercicios que veñen a continuación empregan ditas técnicas. Existe un tipo de ataque que non se ten en conta ao non ter chegado á [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Sesi%C3%B3ns#Ataques_CSRF_.28Cross-Site_Request_Forgery.29 sección de emprego de variables de sesións]. En dita sección se explica o que temos que facer. É importante que vos acordedes de aplicar a técnica descrita cando vexades os contidos.
+
<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
 +
        ]
 +
    );
  
−
<br />
+
    // Operacións coa base de datos...
−
* Para dar de alta novos rexistros na base de datos temos que facer uso da [http://www.w3schools.com/sql/sql_insert.asp instrución SQL INSERT].
+
 
−
: O proceso é moi parecido ao que fixemos coas consultas, coa diferenza que no caso dunha instrución INSERT non temos rexistros de volta.
+
} catch (PDOException $e) {
−
: Soamente debemos comprobar se a inserción a fixo correctamente, comprobando se a chamado ao método query devolve false.
+
 
 +
    // Rexistrar o erro nun log.
 +
    // Nunha aplicación real tamén poderían empregarse
 +
    // ferramentas como Monolog.
 +
    error_log($e->getMessage());
  
−
* Comentar que tamén pode sernos útil a instrución [https://dev.mysql.com/doc/refman/5.7/en/insert.html INSERT IGNORE]. Esta instrución non vai emitir ningún erro se intentamos inserir unha clave duplicada nunha táboa. Loxicamente a operación non se fai.
+
    die('Erro relacionado coa base de datos.');
  
 +
} catch (Throwable $e) {
  
 +
    error_log($e->getMessage());
  
−
* Nestes exemplos, <u>vaise empregar a versión PHP 8.1 ou superior</u> que vai lanzar excepcións en caso de erro como xa vimos [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Control_de_errores no control erros ao abrir a conexión á base de datos].
+
    die('Produciuse un erro na aplicación.');
−
: Podedes consultar como controlar os erros nunha versión PHP anterior [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_CONSULTA#Introducci.C3.B3n consultando a sección de consultas] xa que o procedemento é o mesmo cambiando a orde SQL a executar.
+
}
 +
</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 />
−
====Orientado a obxectos====
 
−
* O proceso para realizar unha inserción é o seguinte:
 
  
−
:* Abrimos unha conexión á base de datos.
+
==== Consultas preparadas ====
−
:* Construímos a orde SQL: INSERT INTO TABOA_1 (col1,col2) VALUES (valor1,'valor2')
+
 
−
:: Neste caso a TABOA_1 está constituída por dúas columnas, a primeira numérica e a segunda de tipo cadea.
+
Sempre que unha consulta conteña datos variables procedentes do usuario ou da aplicación, empregaremos unha '''consulta preparada'''.
−
:* Executamos a orde e comprobamos que a operación foi correcta:
 
−
:* Pechamos a conexión á base de datos.
 
  
 +
Por exemplo, non construiremos unha consulta desta forma:
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
::<syntaxhighlight lang="php" enclose="div">
−
  try {
+
$consulta = sprintf(
 +
    "INSERT INTO estados (descripcion) VALUES ('%s')",
 +
    $descripcion
 +
);
 +
</syntaxhighlight>
  
−
    $conex = new mysqli('SERVIDOR', 'USUARIO', 'PASSWORD', 'BD');
+
Empregaremos un parámetro:
−
    $conex->set_charset('utf8mb4');
+
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'INSERT INTO estados (descripcion) VALUES (:descripcion)'
 +
);
 +
 
 +
$stmt->execute([
 +
    ':descripcion' => $descripcion
 +
]);
 +
</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.
  
−
    $consulta = "INSERT INTO TABOA_1 (col1,col2) VALUES (valor1,'valor2')";  // Envía un dato numérico e un dato cadea. Nos empregaremos %d e %s nunha constante previamente definida e despois sprintf
+
<br />
−
    $conex->query($consulta);
 
  
−
  } catch (mysqli_sql_exception $e) {
+
=== Alta de rexistros: INSERT ===
−
    die("Erro: " . $e->getMessage());
 
−
  }
 
−
  catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
  }
 
−
  finally{
 
−
    if(!empty($conex)){
 
−
      $conex->close();
 
−
    } 
 
−
  }
 
−
}</syntaxhighlight>
 
  
−
:: Fixarse como a consulta a gardamos nunha variable para non ter problemas coas comiñas na chamada á función query.
+
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 />
−
* Cando o rexistro foi dado de alta correctamente podemos comprobar o número de rexistros afectados (no caso dun único rexistro sempre vai devolver 1, pero tamén se podería aplicar a un insert multifila).
+
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="5" >
+
==== Alta empregando PDO ====
−
..................
+
 
−
$result=$conex->query($query);
+
Supoñamos a seguinte táboa:
−
if ($result) {
+
 
−
  echo "Rexistro dado de alta correctamente";
+
::<syntaxhighlight lang="sql" enclose="div">
−
  echo "Num. rexistros afectados:" . $conex->affected_rows;
+
CREATE TABLE TABOA_1 (
−
}
+
    col1 INT PRIMARY KEY AUTO_INCREMENT,
−
$conex->close(); // Pechamos a conexión
+
    col2 VARCHAR(100) NOT NULL
 +
);
 
</syntaxhighlight>
 
</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'''
  
−
<br />
+
::<syntaxhighlight lang="php" enclose="div">
−
====Empregando PDO====
+
<?php
−
* O proceso para realizar unha inserción é o seguinte:
 
  
−
:* Abrimos unha conexión á base de datos.
+
define('SERVIDOR', 'localhost');
−
:* Construímos a orde SQL có paso de dous parámetros: INSERT INTO TABOA_1 (col1,col2) VALUES (:valor1,:valor2)
+
define('USUARIO', 'user_php');
−
:* Facemos o execute enviando os datos aos parámetros.
+
define('PASSWORD', 'user_php');
−
:* Comprobamos que a operación foi correcta:
+
define('BD', 'php');
−
:* Pechamos a conexión á base de datos.
 
  
 +
define(
 +
    'INSERIR_TABOA_1',
 +
    'INSERT INTO TABOA_1 (col2)
 +
    VALUES (:col2)'
 +
);
 +
</syntaxhighlight>
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
'''Arquivo inserir_taboa_1.php'''
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
  
 
try {
 
try {
  
−
     // Abrir conexión coa base de datos
+
     require '/var/www/confBD/bd.php';
 +
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 
     $conex = new PDO(
 
     $conex = new PDO(
−
         "mysql:host=SERVIDOR;dbname=BD;charset=utf8mb4",
+
         $dsn,
−
         'USUARIO',
+
         USUARIO,
−
         'PASSWORD'
+
         PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 
     );
 
     );
  
−
     $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
     $valor = 'Texto de proba';
  
−
     // Construír a orde SQL
+
     $stmt = $conex->prepare(INSERIR_TABOA_1);
−
    // Supoñamos TABOA_1(col1 numérico, col2 cadea)
 
−
    define('INSERIR_TABOA_1', 'INSERT INTO TABOA_1 (col1, col2) VALUES (:col1, :col2)');
 
  
−
     // Preparar a sentenza
+
     $stmt->execute([
−
     $stmt = $conex->prepare(INSERIR_TABOA_1);
+
        ':col2' => $valor
 +
     ]);
 +
 
 +
    echo 'Rexistro dado de alta correctamente.';
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
  
−
     // Valores a inserir
+
     die('Erro relacionado coa base de datos.');
−
    $valor1 = 123;
 
−
    $valor2 = "Texto de proba";
 
  
−
    // Executar a consulta con parámetros
+
} catch (Throwable $e) {
−
    $stmt->execute([':col1' => $valor1, ':col2' => $valor2]);
 
  
−
     echo "Inserción correcta. ID xerado: " . $conex->lastInsertId();
+
     error_log($e->getMessage());
  
−
} catch (PDOException $e) {
+
     die('Produciuse un erro na aplicación.');
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
     die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    // Pechar conexión
 
−
    if (!empty($stmt)) $stmt = null;
 
−
    if (!empty($conex)) $conex = null;
 
 
}
 
}
 +
</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 />
  
−
?>
+
==== Número de filas afectadas ====
  
−
</syntaxhighlight>  
+
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
 +
]);
  
−
<br />
+
$numRexistros = $stmt->rowCount();
−
* Cando o rexistro foi dado de alta correctamente podemos comprobar o número de rexistros afectados (no caso dun único rexistro sempre vai devolver 1, pero tamén se podería aplicar a un insert multifila).
+
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="4-5" >
+
if ($numRexistros === 1) {
−
..................
+
    echo 'Rexistro dado de alta correctamente.';
−
    $stmt->execute([':col1' => $valor1, ':col2' => $valor2]);
+
}
−
    // Comprobar se se inseriu correctamente
 
−
    $numRexistros = $stmt->rowCount(); // Normalmente 1 nun insert
 
−
    if ($numRexistros > 0) {
 
−
        echo "Rexistro dado de alta correctamente<br>";
 
−
        echo "Num. rexistros afectados: " . $numRexistros;
 
−
    } else {
 
−
        echo "Non se inseriu ningún rexistro.";
 
−
    }
 
−
.............
 
 
</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> ou <code>fetchAll()</code>.
  
 
<br />
 
<br />
Línea 156: Línea 245:
 
=== Obtendo o valor dun campo autonumérico ===
 
=== Obtendo o valor dun campo autonumérico ===
  
−
* Normalmente cando deseñamos unha base de datos, certos campos que son claves os definimos como autonuméricos.
+
Cando unha táboa ten unha clave primaria <code>AUTO_INCREMENT</code>, MySQL/MariaDB xera automaticamente o identificador da nova fila.
−
: Ao facelo, cando engadimos unha nova fila a un táboa, xérase un novo valor e en certas ocasións necesitamos obter dito valor xa que o podemos utilizar para gardar outros datos noutras táboas que o necesitan por estar relacionadas.
 
  
−
* Non vale a solución de engadir un dato e inmediatamente facer un 'select' buscando o valor máis alto, xa que non entorno multiusuario, dito valor non ten porqué ser o último engadido.
+
Non debemos inserir unha fila e realizar despois:
  
−
* Necesitamos utilizar a [http://php.net/manual/es/mysqli.insert-id.php función mysqli::$insert_id]].
+
::<syntaxhighlight lang="sql" enclose="div">
 +
SELECT MAX(id) FROM taboa;
 +
</syntaxhighlight>
  
−
: Dita función devolve o id autoxerado que se utilizou na última operación.
+
Nunha aplicación con varios usuarios outra conexión podería inserir unha fila entre ambas as operacións.
  
−
* Por exemplo, imaxinemos que temos a táboa TABLE_1 con dúas columnas, col1 int primary key autogenerate e col2 varchar(100).
+
Con PDO empregaremos:
−
: Cando executamos a orde: INSERT INTO TABLE_1 (col2) VALUES ('valor1')
 
  
−
* O proceso é o seguinte:
+
::<syntaxhighlight lang="php" enclose="div">
−
:* Abrimos a conexión á base de datos.
+
$id = $conex->lastInsertId();
−
:* Executamos a orde SQL que engade unha nova fila.
+
</syntaxhighlight>
  
 +
<code>lastInsertId()</code> devolve o identificador asociado á última inserción realizada pola conexión.
  
−
<br />
+
Exemplo:
−
:::'''Orientado a obxectos'''
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="4" >
 
−
$result=$conex->query("INSERT INTO TABLE_1 (col2) VALUES ('valor')");
 
−
if ($result){
 
−
  // Operación correcta
 
−
  printf ("Novo rexistro có id %d.\n", $conex->insert_id);
 
−
}
 
−
...........
 
−
$conex->close();  // Pechamos a conexión
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(INSERIR_TABOA_1);
  
−
</syntaxhighlight>  
+
$stmt->execute([
 +
    ':col2' => $valor
 +
]);
  
−
<br />
+
$id = $conex->lastInsertId();
−
:::'''Empregando PDO'''
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="7" >
 
−
$stmt = $conex->prepare('INSERT INTO TABOA_1 (col2) VALUES (:col2)');
 
−
$stmt->execute([':col2' => 'valor2']);
 
  
−
$numRexistros = $stmt->rowCount();
+
echo 'Novo rexistro co id ' .
−
if ($numRexistros > 0) {
+
    htmlspecialchars($id, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
−
    echo "Operación correcta<br>";
+
</syntaxhighlight>
−
    echo "Novo rexistro có id " . $conex->lastInsertId() . "<br>";
 
−
}
 
−
</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 />
  
−
=== Alta utilizando procedementos almacenados ===
+
=== Caso práctico: mantemento da táboa ESTADOS ===
  
−
* O proceso de alta é o mesmo que [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_OP_CONSULTA#Consulta_chamando_a_procedementos_almacenados o visto nas consultas], pero utilizando a orde SQL INSERT dentro do procedemento.
+
Imos facer unha pequena parte do mantemento da táboa <code>ESTADOS</code>.
  
 +
Supoñamos:
  
−
* Se estamos a utilizar procedementos almacenados, lembrar que en caso de erro '2014 - Commands out of sync; you can't run this command now', teredes que engadir debemos de engadir a seguinte liña despois de chamar a un procedemento almacenado:
+
::<syntaxhighlight lang="sql" enclose="div">
−
+
CREATE TABLE estados (
−
:::<syntaxhighlight lang="java" enclose="div" highlight="9" >
+
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
−
                    if ($conex->multi_query($proc)){  // Neste caso supoño que o procedemento de alta devolve un conxunto de filas a maiores de facer o INSERT. Non é o normal.
+
    descripcion VARCHAR(100) NOT NULL
−
                        $result = $conex->store_result();
+
);
−
                        if ($result && ($result->num_rows>0)){
 
−
                            while($fila=$result->fetch_assoc()){
 
−
                                // Procesamos cada fila
 
−
                            }
 
−
                            $result->free();
 
−
                        }
 
−
                        while($conex->next_result() && $conex->store_result());
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Crearemos:
  
−
* <b><u>IMPORTANTE:</u></b> Se o procedemento de alta non vai devolver datos (cun select) podemos facer uso da orde $conex->query($proc) sendo $proc='CALL NOME_PROC(parámetros)'
+
:* <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.
 +
:* <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 />
  
−
* <u>Nota:</u> '''Hai que ter unha correspondencia entre o tipo de dato e tamaño do parámetro do procedemento almacenado e a columna da táboa''' onde imos a inserir o dato.
+
==== Arquivo /var/www/confBD/bd.php ====
  
−
:: Así, se a táboa TABOA_1 ten de columnas col1 '''int''', col2 '''varchar(100)'''
+
::<syntaxhighlight lang="php" enclose="div">
−
:: Se defino un procedemento almacenado para dar de alta unha nova fila: CREATE PROCEDURE ALTA_FILA_TABOA_1(_param1 '''int''', _param2 '''varchar(100)''')
+
<?php
  
 +
define('SERVIDOR', 'localhost');
 +
define('USUARIO', 'user_php');
 +
define('PASSWORD', 'user_php');
 +
define('BD', 'php');
  
−
* Indicar que dentro dun procedemento almacenado imos poder ter sentenzas:
+
define(
 +
    'CONSULTAR_ESTADOS',
 +
    'SELECT id, descripcion
 +
    FROM estados
 +
    ORDER BY descripcion'
 +
);
  
−
:* [http://dev.mysql.com/doc/refman/5.7/en/if.html if - else]
+
define(
−
:* [http://dev.mysql.com/doc/refman/5.7/en/while.html while]
+
    'CONSULTAR_ESTADO_NUM',
 +
    'SELECT COUNT(*) AS existe
 +
    FROM estados
 +
    WHERE UPPER(descripcion) = UPPER(:descripcion)'
 +
);
  
−
:* E outras de control de fluxo [http://dev.mysql.com/doc/refman/5.7/en/flow-control-statements.html como podedes ver neste enlace].
+
define(
 +
    'INSERIR_ESTADO',
 +
    'INSERT INTO estados (descripcion)
 +
    VALUES (:descripcion)'
 +
);
 +
</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 />
 
<br />
−
====Caso práctico====
 
  
−
* Imos facer o mantemento da táboa ESTADOS.
+
==== Arquivo campos_estado.php ====
 +
 
 +
::<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.
  
−
:* Imos crear unha páxina de nome '''consultar_estados.php'''. Dita páxina amosará o listado de estados nunha táboa e terá un botón cun formulario na parte superior para dar de alta un novo estado.
+
<br />
−
:* Dito formulario apuntará a unha páxina de nome '''alta_estado.php''' onde se amosará un formulario cun campo de texto para engadir a descricción do estado (lembrar que o id é autonumérico).
 
−
:* O action da páxina 'alta_estado.php' chamará a unha páxina de nome 'procesar_alta_estado.php' o cal fará a operación de INSERT e fará unha redirección á paxina 'consultar_estados' enviando na URL o novo id xerado e unha mensaxe que poderá ser de información (alta dada de alta correctamente) ou de erro.
 
−
:* A páxina alta_estado fará uso dun include de nome campos_estado.php. En dita páxina (campos_estado.php) estará definido a caixa de texto do formulario que debe ser obrigatorio introducir un dato e o tamaño máximo será de 100 caracteres e o mínimo de 4.
 
  
 +
==== Arquivo alta_estado.php ====
  
−
* No exemplo emprégase a [https://www.php.net/manual/es/function.strtoupper.php función strtoupper] para converter o texto a maiúsculas antes de envialo á base de datos.
+
::<syntaxhighlight lang="php" enclose="div">
−
: Cando traballades con tipos de datos cadeas será algo que teredes que ter en conta. Vai depender se no formulario de entrada de datos permitides ou non as minúsculas, ides necesitar empregar esta opción.
+
<!DOCTYPE html>
 +
<html lang="gl">
 +
<head>
 +
    <meta charset="UTF-8">
 +
    <title>Alta de estado</title>
 +
</head>
 +
<body>
  
−
* Outro aspecto importante é que <u>ten que haber unha correspondencia de tipo e tamaño</u> entre o campo da táboa onde imos facer a operación (neste caso a táboa ESTADOS, campo descripcion, que é un varchar(100)) e o parámetro que imos definir no procedemento almacenado.
+
    <h1>Alta de estado</h1>
  
 +
    <form method="post" action="procesar_alta_estado.php">
  
−
* O envío das mensaxes de erro e id a través da URL non é o máis correcto, pero neste punto do curso aínda no vimos o emprego de [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Sesi%C3%B3ns variables de sesión] que sería a forma correcto de facelo.
+
        <?php require 'campos_estado.php'; ?>
  
 +
        <div>
 +
            <button type="submit" name="btnOperacion" value="ALTA">
 +
                Dar de alta
 +
            </button>
 +
        </div>
  
−
<br />
+
    </form>
−
===== Posible Solución: Utilizando orden INSERT orientado a obxectos=====
 
  
−
* Lembrar que dependendo dos casos, teremos que verificar que o valor a engadir non estea dado de alta previamente...
+
</body>
−
'''Páxina bd.php'''
+
</html>
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
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('%s')");
 
−
define('INSERIR_ESTADO',"INSERT INTO estados (descripcion) VALUES ('%s')");
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
 +
==== Arquivo consultar_estados.php ====
  
−
'''Arquivo consultar_estados.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
 
<?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.php');
 
  
 
try {
 
try {
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
     require '/var/www/confBD/bd.php';
−
<!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 estados</title>
 
−
    </head>
 
−
    <body>
 
−
        <form action="alta_estado.php" method="post">
 
−
            <button type="submit">Novo Estado</button>
 
−
        </form>
 
−
        <table border="1" style="margin-top:20px">
 
−
            <tr>
 
−
                <th>Estado</th>
 
−
            </tr>
 
−
        <?php
 
  
−
            $resultado = $conex->query(CONSULTAR_ESTADOS);
+
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
            while($fila = $resultado->fetch_assoc()) {
+
    $conex = new PDO(
−
                $nombre = htmlentities($fila['descripcion']);
+
        $dsn,
−
                echo "<tr>";
+
        USUARIO,
−
                echo "<td>$nombre</td>";
+
        PASSWORD,
−
                echo "</tr>";
+
        [
−
            }
+
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
            $resultado->free();
+
    $stmt = $conex->query(CONSULTAR_ESTADOS);
−
        ?>   
+
    $estados = $stmt->fetchAll();
−
        </table>
 
  
 +
} catch (PDOException $e) {
  
−
        <div>
+
     error_log($e->getMessage());
−
            <?php
 
−
                if(isset($_GET['id'])) printf("<h3>Estado con id %s 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 (Exception $e) {
 
−
     die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
    die('Erro relacionado coa base de datos.');
  
−
'''Arquivo campos_estado.php'''
+
} catch (Throwable $e) {
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
<label for="nombre">Descricción:</label>
 
−
<input type="text" maxlength="100" id="descripcion" name="descripcion" required><br>
 
−
</syntaxhighlight>
 
  
 +
    error_log($e->getMessage());
  
−
'''Arquivo alta_estado.php'''
+
    die('Produciuse un erro na aplicación.');
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
}
 +
?>
 
<!DOCTYPE html>
 
<!DOCTYPE html>
−
<html>
+
<html lang="gl">
 
<head>
 
<head>
−
  <title>Alta Estado</title>
+
    <meta charset="UTF-8">
 +
    <title>Xestión de estados</title>
 
</head>
 
</head>
 
<body>
 
<body>
−
  <h3>ALTA ESTADO</h3>
+
 
−
  <form method="POST" action="procesar_alta_estado.php">
+
    <h1>Xestión de estados</h1>
−
     <?php include 'campos_estado.php'; ?>
+
 
−
     <div style='margin-top:20px'>
+
    <form action="alta_estado.php" method="get">
−
         <input type="submit" value="ALTA" name="btnOperacion">
+
        <button type="submit">Novo estado</button>
−
    </div>
+
     </form>
−
  </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>.
  
−
</syntaxhighlight>
+
<br />
  
 +
==== Arquivo procesar_alta_estado.php ====
  
−
'''Arquivo procesar_alta_estado.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
 
<?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.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'
 +
    ) {
 +
        throw new RuntimeException('Operación non válida.');
 +
    }
 +
 
 +
    $descripcion = trim($_POST['descripcion'] ?? '');
 +
 
 +
    $lonxitude = function_exists('mb_strlen')
 +
        ? mb_strlen($descripcion, 'UTF-8')
 +
        : strlen($descripcion);
 +
 
 +
    if ($lonxitude < 4 || $lonxitude > 100) {
 +
 
 +
        header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode(
 +
                'A descrición debe ter entre 4 e 100 caracteres.'
 +
            )
 +
        );
 +
        exit;
 +
    }
 +
 
 +
    if (function_exists('mb_strtoupper')) {
 +
        $descripcion = mb_strtoupper($descripcion, 'UTF-8');
 +
    } else {
 +
        $descripcion = strtoupper($descripcion);
 +
    }
 +
 
 +
    $stmt = $conex->prepare(CONSULTAR_ESTADO_NUM);
  
−
$error="";  // Variable que a usaremos para amosar os posibles erros que poidan darse
+
     $stmt->execute([
−
$msg='';    // Variable empregada para enviar o novo id xerado
+
         ':descripcion' => $descripcion
−
try {
+
    ]);
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
+
 
−
     $conex->set_charset('utf8mb4');
+
    $fila = $stmt->fetch();
−
    // Xestionamos a operacion do segundo formulario => Alta , Baixa ou Modificacion
+
 
−
    if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion']=='ALTA'){ 
+
    if ($fila !== false && (int) $fila['existe'] > 0) {
−
         if ((strlen($_POST['descripcion'])<4) || (strlen($_POST['descripcion'])>100)){
 
−
            $error = sprintf("error=%s", urlencode("O númnero de caracteres do estado non pode ter máis de 100 caracteres nin menos de 4..."));
 
−
        }
 
−
        else {
 
−
            // Eliminamos caracteres non permitidos
 
−
            $novoEstado = filter_var($_POST['descripcion'],FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
 
−
            // Escapamos as comillas simples e dobres
 
−
            $novoEstado = $conex->real_escape_string(strtoupper($novoEstado));
 
−
            $query=sprintf(CONSULTAR_ESTADO_NUM,$novoEstado);
 
−
            $result = $conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                $fila=$result->fetch_assoc();
 
−
                if ($fila['existe']>0) {
 
−
                    $error = sprintf("error=%s", urlencode("Ese estado xa existe."));
 
−
                }
 
−
                $result->free();
 
−
            }
 
  
−
            if($error==''){
+
        header(
−
                $query=sprintf(INSERIR_ESTADO,$novoEstado);
+
             'Location: consultar_estados.php?error=' .
−
                $result = $conex->query($query);
+
            urlencode('Ese estado xa existe.')
−
                if ($result){
+
        );
−
                    $msg = sprintf("id=%d", $conex->insert_id);
+
         exit;
−
                }
 
−
             }
 
−
        } 
 
−
           
 
−
    }  // Fin da operacion de ALTA
 
−
       
 
−
}  // 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("error='Erro mysql: %s'",htmlentities($e->getMessage()));
 
−
}
 
−
catch (Exception $e) {
 
−
    $error=sprintf("error='Erro xeral: %s'",htmlentities($e->getMessage()));
 
−
}
 
−
finally{
 
−
    if(!empty($conex)){
 
−
         $conex->close();
 
 
     }
 
     }
 +
 +
    $stmt = $conex->prepare(INSERIR_ESTADO);
 +
 +
    $stmt->execute([
 +
        ':descripcion' => $descripcion
 +
    ]);
 +
 +
    $id = (int) $conex->lastInsertId();
 +
 +
    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>
  
−
$url = sprintf("consultar_estados.php?%s&%s",$msg,$error);
+
Neste exemplo debemos destacar varias ideas:
−
header("Location:" . $url);
+
 
 +
:* 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.
 +
:* Non se emprega <code>real_escape_string()</code>.
 +
:* 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 ====
  
−
</syntaxhighlight>
+
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 />
 
<br />
−
===== Posible Solución: Utilizando orden INSERT empregando PDO=====
 
  
−
* Lembrar que dependendo dos casos, teremos que verificar que o valor a engadir non estea dado de alta previamente...
+
=== Alta utilizando procedementos almacenados ===
−
'''Páxina bd.php'''
+
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
Podemos encapsular parte da lóxica nun procedemento almacenado de MySQL/MariaDB.
−
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)");
 
−
</syntaxhighlight>
 
  
 +
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>.
  
−
'''Arquivo consultar_estados.php'''
+
<br />
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
<?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.php');
+
==== Procedemento almacenado ====
  
−
try {
+
::<syntaxhighlight lang="sql" enclose="div">
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
DELIMITER $$
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
     $erro = "";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
CREATE PROCEDURE INSERIR_ESTADO_PROC(
−
?>
+
     IN p_descripcion VARCHAR(100)
 +
)
 +
etiqueta: BEGIN
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
     IF (
−
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
         SELECT COUNT(*)
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
         FROM estados
−
    <head>
+
         WHERE UPPER(descripcion) = UPPER(p_descripcion)
−
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
    ) > 0 THEN
−
         <title>Xestión de estados</title>
 
−
    </head>
 
−
    <body>
 
−
        <form action="alta_estado.php" method="post">
 
−
            <button type="submit">Novo Estado</button>
 
−
         </form>
 
−
        <table border="1" style="margin-top:20px">
 
−
            <tr>
 
−
                <th>Estado</th>
 
−
            </tr>
 
−
        <?php
 
−
            $result=$conex->query(CONSULTAR_ESTADOS);    // Temos que poñer prepare
 
−
            if ($result && $result->rowCount() > 0) {
 
−
                while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
 
−
                    $nombre = htmlentities($row['descripcion']);
 
−
                    echo "<tr>";
 
−
                    echo "<td>$nombre</td>";
 
−
                    echo "</tr>";
 
−
                }
 
−
            }
 
  
−
            $result = null;
+
        SELECT -1 AS valor_devolto;
−
         ?>   
+
         LEAVE etiqueta;
−
        </table>
 
  
 +
    END IF;
  
−
        <div>
+
     INSERT INTO estados (descripcion)
−
            <?php
+
     VALUES (p_descripcion);
−
                if(isset($_GET['id'])) printf("<h3>Estado con id %s 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 do principio da páxina
 
−
catch (PDOException $e) {
 
−
     die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
     die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
    SELECT LAST_INSERT_ID() AS valor_devolto;
  
 +
END$$
  
−
'''Arquivo campos_estado.php'''
+
DELIMITER ;
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
<label for="nombre">Descricción:</label>
 
−
<input type="text" maxlength="100" id="descripcion" name="descripcion" required>
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
−
'''Arquivo alta_estado.php'''
+
==== Arquivo /var/www/confBD/bd.php ====
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
−
<!DOCTYPE html>
+
Engadimos a chamada ao procedemento:
−
<html>
 
−
<head>
 
−
  <title>Alta Estado</title>
 
−
</head>
 
−
<body>
 
−
  <h3>ALTA ESTADO</h3>
 
−
  <form method="POST" action="procesar_alta_estado.php">
 
−
    <?php include 'campos_estado.php'; ?>
 
−
    <div style='margin-top:20px'>
 
−
        <input type="submit" value="ALTA" name="btnOperacion">
 
−
    </div>
 
−
  </form>
 
−
</body>
 
−
</html>
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'PROCEDEMENTO_INSERIR_ESTADO',
 +
    'CALL INSERIR_ESTADO_PROC(:descripcion)'
 +
);
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
−
'''Arquivo procesar_alta_estado.php'''
+
==== Chamando ao procedemento con PDO ====
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
 +
'''Arquivo procesar_alta_estado_proc.php'''
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?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.php');
 
  
−
$error="";  // Variable que a usaremos para amosar os posibles erros que poidan darse
 
−
$msg='';    // Variable empregada para enviar o novo id xerado
 
 
try {
 
try {
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",SERVIDOR,BD);
 
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
     // Xestionamos a operacion do segundo formulario => Alta , Baixa ou Modificacion
+
     require '/var/www/confBD/bd.php';
−
    if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion']=='ALTA'){ 
 
−
        if ((strlen($_POST['descripcion'])<4) || (strlen($_POST['descripcion'])>100)){
 
−
            $error = sprintf("error=%s", urlencode("O númnero de caracteres do estado non pode ter máis de 100 caracteres nin menos de 4..."));
 
−
        }
 
−
        else {
 
−
            // Eliminamos caracteres non permitidos
 
−
            $novoEstado = filter_var($_POST['descripcion'],FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
 
−
           
 
−
            $stmt=$conex->prepare(CONSULTAR_ESTADO_NUM);    // Temos que poñer prepare
 
−
            $stmt->bindValue(':descripcion',$novoEstado,PDO::PARAM_STR);
 
−
            $result = $stmt->execute();
 
  
−
            if($result){
+
    $dsn = sprintf(
−
                while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) {
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
                    if ($fila['existe']>0) {
+
        SERVIDOR,
−
                        $error = sprintf("error=%s", "Ese estado xa existe.");
+
        BD
−
                    }
+
    );
−
                }
 
−
            }
 
−
            $stmt = null;
 
  
−
            if($error==''){
+
    $conex = new PDO(
−
                $stmt=$conex->prepare(INSERIR_ESTADO);    // Temos que poñer prepare
+
        $dsn,
−
                $stmt->bindValue(':descripcion',$novoEstado,PDO::PARAM_STR);
+
        USUARIO,
−
                $result = $stmt->execute();
+
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
                if ($result){
+
    if (
−
                    $msg = sprintf("id=%d", $conex->lastInsertId());
+
        !isset($_POST['btnOperacion']) ||
−
                }
+
         $_POST['btnOperacion'] !== 'ALTA'
−
            }
+
    ) {
−
        } 
+
        throw new RuntimeException('Operación non válida.');
−
           
 
−
    }  // Fin da operacion de ALTA
 
−
          
 
−
}  // 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 (PDOException $e) {
 
−
    $error=sprintf("error='Erro mysql: %s'",urlencode($e->getMessage()));
 
−
}
 
−
catch (Exception $e) {
 
−
    $error=sprintf("error='Erro xeral: %s'",urlencode($e->getMessage()));
 
−
}
 
−
finally{
 
−
    if(!empty($conex)){
 
−
        $conex = null;
 
 
     }
 
     }
−
}
 
  
−
$url = sprintf("consultar_estados.php?%s&%s",$msg,$error);
+
    $descripcion = trim($_POST['descripcion'] ?? '');
−
header("Location:" . $url);
+
 
 +
    $lonxitude = function_exists('mb_strlen')
 +
        ? mb_strlen($descripcion, 'UTF-8')
 +
        : strlen($descripcion);
 +
 
 +
    if ($lonxitude < 4 || $lonxitude > 100) {
  
−
?>
+
        header(
 +
            'Location: consultar_estados.php?error=' .
 +
            urlencode(
 +
                'A descrición debe ter entre 4 e 100 caracteres.'
 +
            )
 +
        );
 +
        exit;
 +
    }
  
−
</syntaxhighlight>
+
    $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();
  
−
<br />
+
    if ($fila === false) {
  
−
===== Posible Solución: Utilizando procedementos almacenados orientada a obxectos e PDO=====
+
        throw new RuntimeException(
 +
            'O procedemento non devolveu ningún resultado.'
 +
        );
 +
    }
  
 +
    $valorDevolto = (int) $fila['valor_devolto'];
  
−
* A diferenza do caso anterior:
+
    if ($valorDevolto === -1) {
  
−
:* O id xerado terá que devolvelo o procedemento, mediante SELECT, un parámetro de SAÍDA ou un RETURN se estamos a utilizar unha función.
+
        header(
−
::: Teremos que utilizar a [https://dev.mysql.com/doc/refman/5.5/en/information-functions.html#function_last-insert-id función last_insert_id()] para obter o id xerado dende o procedemento de Mysql.
+
            'Location: consultar_estados.php?error=' .
 +
            urlencode('Ese estado xa existe.')
 +
        );
 +
        exit;
 +
    }
  
−
:* A comprobación de se o dato existe o facemos dentro do procedemento.
+
    header(
 +
        'Location: consultar_estados.php?id=' .
 +
        urlencode((string) $valorDevolto)
 +
    );
 +
    exit;
  
 +
} catch (PDOException $e) {
  
−
'''Procedemento almacenado creado en Mysql'''
+
    error_log($e->getMessage());
−
::<syntaxhighlight lang="java" enclose="div" highlight="4,22" >
 
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_ESTADO`(p_descripcion varchar(100))
+
    header(
−
etiqueta:BEGIN  -- Asociamos unha etiqueta ao código do procedemento para que poidamos facer algo parecido a un exit
+
        'Location: consultar_estados.php?error=' .
−
    declare existe int;
+
        urlencode('Erro relacionado coa base de datos.')
−
+
    );
−
SELECT count(*)  
+
    exit;
−
INTO existe
 
−
FROM estados
 
−
WHERE upper(descripcion)=upper(p_descripcion);
 
  
−
IF existe=1 THEN
+
} catch (Throwable $e) {
−
begin
 
−
SELECT -1 as valor_devolto;
 
−
leave etiqueta; -- Para que saia do procedemento
 
−
end;
 
−
END IF;  -- Fin do IF
 
  
−
INSERT INTO estados (descripcion)
+
    error_log($e->getMessage());
−
VALUES (p_descripcion);
 
  
−
SELECT last_insert_id() as valor_devolto;
+
    header(
 +
        'Location: consultar_estados.php?error=' .
 +
        urlencode('Produciuse un erro na aplicación.')
 +
    );
 +
    exit;
 +
}
 +
</syntaxhighlight>
  
−
END
+
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.
  
−
DELIMITER ;
+
<br />
−
</syntaxhighlight>  
 
  
−
* Liña 4: Utilizamos unha etiqueta para que poidamos utilizar a [https://dev.mysql.com/doc/refman/5.7/en/leave.html orde LEAVE] xa que Mysql non ten outra forma de poder saír dun procedemento almacenado.
+
=== Tratamento dos datos e caracteres especiais ===
−
:: Neste caso en concreto quizais podería ser más aconsellable utilizar unha función, xa que estamos a devolver un único dato (neste caso -1 ou a id xerada). Desta maneira, poñendo un return xa sairiamos da función.
 
−
* Liña 22: Utilizamos a funciuón last_insert_id() para obter o último id xerado da inserción.
 
  
 +
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.
  
−
* Comentar que dentro dos xestores de base de datos, nos procedementos/funcións podemos empregar as consultas directamente como expresións, por exemplo, no caso anterior:
+
Non debemos mesturar estas tres operacións.
−
::<syntaxhighlight lang="java" enclose="div" highlight="6-11" >
 
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_ESTADO`(p_descripcion varchar(100))
+
<br />
−
etiqueta:BEGIN  -- Asociamos unha etiqueta ao código do procedemento para que poidamos facer algo parecido a un exit
 
  
−
IF (SELECT count(*) FROM estados WHERE upper(descripcion)=upper(p_descripcion))>0 THEN
+
==== SQL Injection ====
−
begin
 
−
SELECT -1 as valor_devolto;
 
−
leave etiqueta; -- Para que saia do procedemento
 
−
end;
 
−
END IF;  -- Fin do IF
 
  
−
INSERT INTO estados (descripcion)
+
Para introducir valores variables nunha consulta SQL empregaremos consultas preparadas:
−
VALUES (p_descripcion);
 
  
−
SELECT last_insert_id() as valor_devolto;
+
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    'INSERT INTO autores (nome)
 +
    VALUES (:nome)'
 +
);
  
−
END$$
+
$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:
  
−
'''Páxina bd.php versión orientado a obxectos'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
$descripcion = trim($_POST['descripcion'] ?? '');
−
define('PROCEDEMENTO_INSERIR_ESTADO',"INSERIR_ESTADO('%s')");
+
 
 +
if ($descripcion === '') {
 +
    die('A descrición é obrigatoria.');
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
'''Páxina bd.php empregando PDO'''
+
A normalización transforma un valor á representación decidida pola aplicación.
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
−
define('PROCEDEMENTO_INSERIR_ESTADO',"INSERIR_ESTADO(:novo_estado)");
+
Por exemplo:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$descripcion = mb_strtoupper(
 +
    $descripcion,
 +
    'UTF-8'
 +
);
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Ningunha destas operacións substitúe unha consulta preparada.
  
 +
<br />
  
−
'''Páxina procesar_alta_estado.php versión orientada a obxectos'''
+
==== Saída HTML e XSS ====
−
::<syntaxhighlight lang="java" enclose="div" highlight="35-53" >
 
−
<?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.php');
+
Se un dato se vai inserir como texto nun documento HTML, debemos escapar a saída:
  
−
$error="";  // Variable que a usaremos para amosar os posibles erros que poidan darse
+
::<syntaxhighlight lang="php" enclose="div">
−
$msg='';    // Variable empregada para enviar o novo id xerado
+
echo htmlspecialchars(
−
try {
+
     $fila['descripcion'],
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
+
     ENT_QUOTES | ENT_SUBSTITUTE,
−
     $conex->set_charset('utf8mb4');
+
     'UTF-8'
−
     // Xestionamos a operacion do segundo formulario => Alta , Baixa ou Modificacion
+
);
−
     if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion']=='ALTA'){ 
+
</syntaxhighlight>
−
        if ((strlen($_POST['descripcion'])<4) || (strlen($_POST['descripcion'])>100)){
 
−
            $error = sprintf("error=%s", urlencode("A lonxitude do estado non pode ter máis de 100 caracteres nin menos de 4..."));
 
−
        }
 
−
        else {
 
−
            // Eliminamos caracteres non permitidos
 
−
            $novoEstado = filter_var($_POST['descripcion'],FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
 
  
−
                $proc=sprintf(PROCEDEMENTO_INSERIR_ESTADO,$novoEstado);
+
Isto debe facerse '''ao amosar o dato en HTML''', non necesariamente antes de gardalo na base de datos.
−
                $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:
 
−
                                $error = sprintf("error=%s", urlencode("Ese valor xa está dado de alta na base de datos..."));
 
−
                                break;
 
−
                            default:
 
−
                                $msg = sprintf("id=%d", $fila['valor_devolto']);
 
−
                        }
 
−
                        $result->free();
 
−
                    }
 
−
                    else{  // Sempre ten que devolver algo como temos implementado o proc.
 
−
                        $error = sprintf("error=%s", urlencode("Houbo un erro na chamada á base de datos"));
 
−
                    }
 
−
                }
 
−
        } 
 
−
           
 
−
    }  // Fin da operacion de ALTA
 
−
       
 
−
}  // 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("error='Erro mysql: %s'",htmlentities($e->getMessage()));
 
−
}
 
−
catch (Expcetion $e) {
 
−
    $error=sprintf("error='Erro xeral: %s'",htmlentities($e->getMessage()));
 
−
}
 
−
finally{
 
−
    if(!empty($conex)){
 
−
        $conex->close();
 
−
    }
 
−
}
 
  
−
$url = sprintf("consultar_estados.php?%s&%s",$msg,$error);
+
Por exemplo, se un usuario introduce:
−
header("Location:" . $url);
 
  
−
?>      
+
::<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.
  
−
* Neste caso é necesario utilizar a <u>opción multi_query</u> xa que recuperamos un dato que devolve o procedemento.
+
<br />
  
 +
=== Transaccións ===
  
−
<br />
+
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.
−
'''Páxina procesar_alta_estado.php empregando PDO'''
+
Isto será [https://wiki.cifprodolfoucha.es/index.php?title=PHP_UD3_TRANSACCIONS explicado posteriormente nun punto da wiki].
−
::<syntaxhighlight lang="java" enclose="div" highlight="35-53" >
+
 
 +
'''Arquivo /var/www/confBD/bd.php'''
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
define(
 +
    'INSERIR_PEDIDO',
 +
    'INSERT INTO pedidos (cliente)
 +
    VALUES (:cliente)'
 +
);
 +
 
 +
define(
 +
    'INSERIR_LINA_PEDIDO',
 +
    'INSERT INTO lina_pedido (pedido_id, produto_id, cantidade)
 +
    VALUES (:pedido_id, :produto_id, :cantidade)'
 +
);
 +
</syntaxhighlight>
 +
 
 +
'''Exemplo de uso'''
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o script sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
+
try {
  
−
$error = "";
+
    require '/var/www/confBD/bd.php';
−
$msg = "";
+
 
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
try {
 
−
    // Conexión con PDO
 
 
     $conex = new PDO(
 
     $conex = new PDO(
−
         "mysql:host=" . SERVIDOR . ";dbname=" . BD . ";charset=utf8mb4",
+
         $dsn,
 
         USUARIO,
 
         USUARIO,
−
         PASSWORD
+
         PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 
     );
 
     );
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
     // Xestionamos a operación do segundo formulario => ALTA
+
     $conex->beginTransaction();
−
    if (!empty($_POST['btnOperacion']) && $_POST['btnOperacion'] === 'ALTA') {
 
  
−
        if (strlen($_POST['descripcion']) < 4 || strlen($_POST['descripcion']) > 100) {
+
    $stmtPedido = $conex->prepare(INSERIR_PEDIDO);
−
            $error = sprintf("error=%s", urlencode("A lonxitude do estado non pode ter máis de 100 caracteres nin menos de 4..."));
 
−
        } else {
 
  
−
            // Eliminamos caracteres non permitidos e poñemos en maiúsculas
+
    $stmtPedido->execute([
−
            $novoEstado = filter_var($_POST['descripcion'], FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES);
+
        ':cliente' => 'Ana'
−
            $novoEstado = strtoupper($novoEstado);
+
    ]);
  
−
            $stmt = $conex->prepare("CALL " . PROCEDEMENTO_INSERIR_ESTADO);
+
    $pedidoId = (int) $conex->lastInsertId();
−
            $stmt->bindValue(':novo_estado', $novoEstado, PDO::PARAM_STR);
 
−
            $stmt->execute();
 
  
−
            // O procedemento debería devolver un resultado
+
    $stmtLinha = $conex->prepare(
−
            $fila = $stmt->fetch(PDO::FETCH_ASSOC);
+
        INSERIR_LINA_PEDIDO
−
            if ($fila) {
+
    );
−
                switch ($fila['valor_devolto']) {
+
 
−
                    case -1:
+
    $stmtLinha->execute([
−
                        $error = sprintf("error=%s", urlencode("Ese valor xa está dado de alta na base de datos..."));
+
        ':pedido_id' => $pedidoId,
−
                        break;
+
        ':produto_id' => 10,
−
                    default:
+
        ':cantidade' => 2
−
                        $msg = sprintf("id=%d", $fila['valor_devolto']);
+
    ]);
−
                }
+
 
−
            } else {
+
    $conex->commit();
−
                $error = sprintf("error=%s", urlencode("Houbo un erro na chamada á base de datos"));
+
 
−
            }
+
} catch (PDOException $e) {
−
         }
+
 
 +
    if (
 +
        isset($conex) &&
 +
        $conex->inTransaction()
 +
    ) {
 +
         $conex->rollBack();
 
     }
 
     }
  
−
} catch (PDOException $e) {
+
     error_log($e->getMessage());
−
     $error = sprintf("error='Erro PDO: %s'", urlencode($e->getMessage()));
+
 
−
} catch (Exception $e) {
+
     die('Erro relacionado coa base de datos.');
−
     $error = sprintf("error='Erro xeral: %s'", urlencode($e->getMessage()));
+
 
−
} finally {
+
} catch (Throwable $e) {
−
    // Pechar recursos
 
−
    if (!empty($stmt)) $stmt = null;
 
−
    if (!empty($conex)) $conex = null;
 
−
}
 
  
−
// Redirección co resultado
+
    if (
−
$url = sprintf("consultar_estados.php?%s&%s", $msg, $error);
+
        isset($conex) &&
−
header("Location:" . $url);
+
        $conex->inTransaction()
−
exit;
+
    ) {
 +
        $conex->rollBack();
 +
    }
  
−
?>
+
    error_log($e->getMessage());
  
 +
    die('Produciuse un erro na aplicación.');
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
=== Como tratar aos caracteres 'estraños' ===
+
A transacción garante que non quede gardada só unha parte da operación.
  
−
* Con caracteres 'estraños' queremos dicir caracteres fora do abecedario e caracteres numéricos, coma por exemplo: ', '' ,\n,....
+
<br />
  
−
* Aquí debemos diferenciar dous casos.
+
=== mysqli como ampliación ===
−
:* Cando queremos gardar caracteres estraños dentro da base de datos.
 
−
:* Cando queremos impedir que o usuario poida utilizar ese tipo de caracteres.
 
  
 +
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.
  
−
==== Necesitamos gardar caracteres 'estraños' na base de datos ====
+
Se empregamos <code>mysqli</code>, tamén debemos utilizar consultas preparadas cando existan datos variables.
  
−
* Neste caso, teremos que 'codificar' e 'descodificar' os datos.
+
<br />
−
:: Codificaremos os datos cando os gardemos (ou modifiquemos) coa orde SQL INSERT e UPDATE.
 
−
:: Decodificaremos: Pode non ser necesario, dependerá do dato e o que queiramos facer con él.
 
  
−
* As funcións que podemos empregar para isto son:
+
==== Conexión con mysqli ====
−
:* [http://php.net/manual/es/function.htmlentities.php Función htmlentities]: Codifica os caracteres en entidades html. Por exemplo, '<' o codifica como '&lt'. Fixarse nas 'opcións' xa que por defecto codifica as comiñas dobres pero non as simples.
 
−
:* [http://php.net/manual/es/function.html-entity-decode.php Función html_entity_decode]: Realiza o proceso inverso ao anterior.
 
  
−
* Por exemplo, imaxinade que o usuario engade nunha caixa de texto un código HTML que queredes gardar na base de datos, pero que o navegador non debe 'interpretar'. Por exemplo a cadea: <script>alert('ola);</script>
+
Exemplo equivalente de conexión:
−
: Isto podería ser interpretado como un ataque [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios como xa vimos anteriormente]. Codificando a cadea coa función htmlentities isto non é posible.
 
−
: Cando amosemos o resultado de aplicar a función, non deberemos decodificalo, xa que o que queremos é que o navegador 'amose' a cadea, non que 'interprete' o código e execute o código javascript.
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
  
−
: <u>IMPORTANTE:</u> Lembrar que para utilizar esta solución, todos os datos (ou os que nos interesen que poidan ter este tipo de caracteres) deben ser codificados.
+
try {
−
: Se <u>un usuario fai unha busca</u> sobre algún destes campos, teremos que codificar tamén a cadea escrita polo usuario.
 
  
 +
    require '/var/www/confBD/bd.php';
  
 +
    mysqli_report(
 +
        MYSQLI_REPORT_ERROR |
 +
        MYSQLI_REPORT_STRICT
 +
    );
  
 +
    $conex = new mysqli(
 +
        SERVIDOR,
 +
        USUARIO,
 +
        PASSWORD,
 +
        BD
 +
    );
  
−
* Outra forma de 'codificar' caracteres 'estraños' é empregando a [http://php.net/manual/es/mysqli.real-escape-string.php función  mysqli::real_escape_string].
+
    $conex->set_charset('utf8mb4');
−
: Esta función, aplicada a unha cadea de caracteres, 'escapa' os seguintes caracteres:  NUL (ASCII 0), \n, \r, \, ', ", e Control-Z. Isto quere dicir que antepón a ditos caracteres o carácter \ e isto fai que o Mysql interprete o seguinte carácter como o que é.
 
  
 +
    // Operacións coa base de datos...
  
−
<br />
+
} catch (mysqli_sql_exception $e) {
−
:'''Nota:'''<u>Deberemos aplicar sempre esta función para impedir a inxección de código</u> se non aplicamos outra función que 'codifique' as comiñas simples, antes de enviar os datos á base de datos.
 
  
 +
    error_log($e->getMessage());
  
−
: Vexamos un exemplo coa seguinte cadea:
+
    die('Erro relacionado coa base de datos.');
−
:: $valor = "O'Really";
 
−
:: Queremos facer a seguinte consulta: INSERT INTO AUTORES (nome) VALUES ('$valor')
 
−
:: Se substituímos $valor polo que vale, vemos que temos un problema coas comiñas simples e daría un erro de sintaxe SQL.
 
  
−
:* Para solucionalo,podemos aplicar a función 'real_escape_string' a $valor da forma:
+
} catch (Throwable $e) {
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
    error_log($e->getMessage());
−
<?php
 
−
$mysqli = new mysqli("localhost", "usuario", "contrasinal", "bd");
 
  
−
if (mysqli_connect_errno()) {
+
     die('Produciuse un erro na aplicación.');
−
     printf("Erro na conexión: %s\n", mysqli_connect_error());
 
−
    exit();
 
 
}
 
}
 +
</syntaxhighlight>
 +
 +
<br />
  
 +
==== INSERT preparado con mysqli ====
  
−
$valor = "O'Really";
+
Se no arquivo <code>bd.php</code> temos:
  
−
/* esta consulta faia debido a que non escapa $cidade */
+
::<syntaxhighlight lang="php" enclose="div">
−
if (!$mysqli->query("INSERT INTO AUTORES (nome) VALUES ('$valor')")) {
+
define(
−
    printf("Erro: %s\n", $mysqli->sqlstate);
+
    'INSERIR_ESTADO_MYSQLI',
−
}
+
    'INSERT INTO estados (descripcion)
 +
    VALUES (?)'
 +
);
 +
</syntaxhighlight>
 +
 
 +
podemos facer:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$stmt = $conex->prepare(
 +
    INSERIR_ESTADO_MYSQLI
 +
);
  
−
$valor = $mysqli->real_escape_string($valor);
+
$stmt->bind_param(
 +
    's',
 +
    $descripcion
 +
);
  
−
/* esta consulta con $valor escapada funcionará */
+
$stmt->execute();
−
if (!$mysqli->query("INSERT INTO AUTORES (nome) VALUES ('$valor')")) {
 
−
    printf("%d fila inserida.\n", $mysqli->affected_rows);
 
−
}
 
  
−
$mysqli->close();
+
$id = $conex->insert_id;
−
?>
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
A letra <code>s</code> indica que o parámetro é unha cadea.
  
−
* '''Se estamos a <u>empregar PDO</u> xa non necesitamos facer uso da función real_escape_string, xa que o fai automaticamente.'''
+
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>
 +
 +
Isto evita ter que construír manualmente a consulta SQL concatenando ou interpolando valores.
 +
 +
<br />
  
 +
=== Resumo ===
  
−
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2025).
+
* 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 del 09:49 22 sep 2026

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.


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_EXCEPTION para que o comportamento quede claro.
  • Indicamos tamén PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, polo que os métodos fetch() e fetchAll() 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() 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.
  • procesar_alta_estado.php: valida os datos e realiza o INSERT.
  • /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'
    ) {
        throw new RuntimeException('Operación non válida.');
    }

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

    $lonxitude = function_exists('mb_strlen')
        ? mb_strlen($descripcion, 'UTF-8')
        : strlen($descripcion);

    if ($lonxitude < 4 || $lonxitude > 100) {

        header(
            'Location: consultar_estados.php?error=' .
            urlencode(
                'A descrición debe ter entre 4 e 100 caracteres.'
            )
        );
        exit;
    }

    if (function_exists('mb_strtoupper')) {
        $descripcion = mb_strtoupper($descripcion, 'UTF-8');
    } else {
        $descripcion = strtoupper($descripcion);
    }

    $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();

    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.
  • Non se emprega real_escape_string().
  • 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égase exit para 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 -1 se xa existe;
  • realiza o INSERT en 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'
    ) {
        throw new RuntimeException('Operación non válida.');
    }

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

    $lonxitude = function_exists('mb_strlen')
        ? mb_strlen($descripcion, 'UTF-8')
        : strlen($descripcion);

    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) {

        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:

  1. evitar SQL Injection;
  2. validar ou normalizar datos segundo as regras da aplicación;
  3. 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.

Arquivo /var/www/confBD/bd.php

define(
    'INSERIR_PEDIDO',
    'INSERT INTO pedidos (cliente)
     VALUES (:cliente)'
);

define(
    'INSERIR_LINA_PEDIDO',
    'INSERT INTO lina_pedido (pedido_id, produto_id, cantidade)
     VALUES (:pedido_id, :produto_id, :cantidade)'
);

Exemplo de uso

<?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
        ]
    );

    $conex->beginTransaction();

    $stmtPedido = $conex->prepare(INSERIR_PEDIDO);

    $stmtPedido->execute([
        ':cliente' => 'Ana'
    ]);

    $pedidoId = (int) $conex->lastInsertId();

    $stmtLinha = $conex->prepare(
        INSERIR_LINA_PEDIDO
    );

    $stmtLinha->execute([
        ':pedido_id' => $pedidoId,
        ':produto_id' => 10,
        ':cantidade' => 2
    ]);

    $conex->commit();

} catch (PDOException $e) {

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

    error_log($e->getMessage());

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

} catch (Throwable $e) {

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

    error_log($e->getMessage());

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

A transacción garante que non quede gardada só unha parte da operación.


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.


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() e execute().
  • Non concatenaremos datos recibidos do usuario dentro dunha orde SQL.
  • Para obter un identificador AUTO_INCREMENT empregaremos PDO::lastInsertId().
  • Para coñecer as filas afectadas por un INSERT, UPDATE ou DELETE podemos empregar PDOStatement::rowCount().
  • Non empregaremos rowCount() como método portable para contar os resultados dun SELECT.
  • 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.
  • mysqli explicarase como alternativa adicional, tamén empregando consultas preparadas.