Diferencia entre revisiones de «PHP UD3 OP CONSULTA»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 74 ediciones intermedias del mismo usuario)
Línea 3: Línea 3:
  
 
* '''<u>Requerimentos 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] e 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]
 
* '''<u>Requerimentos 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] e 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]
 +
 +
 +
'''IMPORTANTE:''' Nos exames, exercicios e prácticas avaliables empregarase '''PDO''' para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario.
 +
Os exemplos con mysqli poden manterse nos apuntes a nivel informativo, pero os exemplos de referencia serán os realizados con PDO.
  
  
  
 
<br />
 
<br />
−
* Para conectarnos primeiro temos que ter a consulta SQL.  
+
* O proceso para realizar unha consulta é o seguinte:
−
* Indicar que o [https://es.wikipedia.org/wiki/SQL SQL (Structure Query Language)] é a linguaxe utilizada para obter datos dunha base de datos relacional.
+
:* '''Paso 1: Abrimos a conexión á base de datos''': [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Apertura_da_conexi.C3.B3n Isto xa o vimos nunha sección anterior]
 +
 
 +
 
 +
* Necesitamos ter unha consulta SQL para recuperar datos.  
 +
: Indicar que o [https://es.wikipedia.org/wiki/SQL SQL (Structure Query Language)] é a linguaxe utilizada para obter datos dunha base de datos relacional.
 +
: Indicar que estos contidos xa deben estar aprendidos do módulo de Base de datos e saber facer joins entre táboas, subconsultas, agrupacións,...
  
 +
 +
* '''CONSIDERACIÓNS:'''
 +
:* Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos)
 +
:* Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro vimos nun punto anterior desta sección].
 +
:* Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas.
 +
:* En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes.
 +
:* Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...).
 +
:* Lembrar que tamén podedes activar o debugger como está indicado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Habilitar_depuraci%C3%B3n_en_VS_Code nesta sección da wiki].
 +
 +
 +
 +
<br />
 +
 +
=== Realización dunha consulta SELECT ===
 +
 +
* '''Paso 2: Chamar ao procedemento query enviando como parámetro a consulta a realizar e a conexión aberta previamente.'''
 
: Consulta SQL: '''SELECT col1, col2 FROM TABOA_1 ORDER BY col'''
 
: Consulta SQL: '''SELECT col1, col2 FROM TABOA_1 ORDER BY col'''
  
 +
* Se a consulta SQL '''non leva datos externos''' podemos empregar directamente o método query():
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
ORDER BY col1'
 +
);
 +
</syntaxhighlight>
 +
: query() devolve un obxecto da clase PDOStatement.
  
−
* Funcións para realizar consultas a base de datos e funcións relacionadas:
+
 
 +
* Se a consulta leva datos procedentes do usuario ou valores que queremos enviar mediante parámetros empregaremos:
 +
::<syntaxhighlight lang="php">
 +
prepare()
 +
execute()
 +
</syntaxhighlight>
 +
: Por exemplo:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor'
 +
);
 +
$stmt->execute([
 +
'valor' => 5
 +
]);
 +
</syntaxhighlight>
 +
 
 +
 
 +
 
 +
 
 +
<br />
 +
* '''Version procedimental'''
 +
: Funcións para realizar consultas a base de datos e funcións relacionadas:
  
 
:* [http://php.net/manual/es/mysqli.query.php mysqli::query]: Realiza a consulta
 
:* [http://php.net/manual/es/mysqli.query.php mysqli::query]: Realiza a consulta
Línea 29: Línea 86:
  
  
 +
::* Versión procedimental:
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
$result=mysqli_query($conex, 'SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
 +
</syntaxhighlight>
 +
::<u>Nota:</u> $conex é a conexión aberta feita no paso anterior.
  
 +
::* Versión orientada a obxectos e PDO:
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
$result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
 +
</syntaxhighlight>
  
 +
::* $result vai ter o conxunto de filas que devolva o resultado. Podemos velo como unha táboa formada polas filas do resultado e polas columnas indicadas no select.
  
  
  
 
<br />
 
<br />
−
* O proceso para realizar unha consulta é o seguinte:
 
−
:* '''Paso 1: Abrimos a conexión á base de datos''': [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Apertura_da_conexi.C3.B3n Isto xa o vimos nunha sección anterior]
 
  
 +
=== Comprobar se unha consulta SELECT devolveu datos ===
 +
 +
* '''Non debemos empregar rowCount() para comprobar o número de filas devoltas por un SELECT'''.
 +
: rowCount() está pensado principalmente para coñecer o número de filas afectadas por operacións INSERT, UPDATE ou DELETE.
 +
: Nun SELECT o seu comportamento depende do controlador da base de datos e non está garantido.
 +
 +
 +
* Unha forma sinxela de comprobar se hai datos consiste en realizar un primeiro fetch():
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
ORDER BY col1'
 +
);
 +
$fila = $stmt->fetch();
 +
if ($fila !== false) {
 +
do {
 +
 +
    echo htmlspecialchars(
 +
        $fila['col1'],
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    );
 +
 +
    echo ' - ';
 +
 +
    echo htmlspecialchars(
 +
        $fila['col2'],
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    );
 +
 +
    echo '<br>';
 +
 +
} while ($fila = $stmt->fetch());
 +
} else {
 +
echo 'Non hai datos que amosar';
 +
}
 +
</syntaxhighlight>
 +
 +
* Se sabemos que o número de filas vai ser pequeno tamén podemos recuperar todas as filas mediante '''fetchAll()''':
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
ORDER BY col1'
 +
);
 +
$filas = $stmt->fetchAll();
 +
if ($filas) {
 +
foreach ($filas as $fila) {
 +
 +
    echo htmlspecialchars(
 +
        $fila['col1'],
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    );
 +
 +
    echo ' - ';
  
 +
    echo htmlspecialchars(
 +
        $fila['col2'],
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    );
  
 +
    echo '<br>';
 +
}
 +
} else {
 +
echo 'Non hai datos que amosar';
 +
}
 +
</syntaxhighlight>
  
  
−
<br />
+
* [https://www.php.net/manual/es/pdostatement.fetchcolumn.php fetchColum] permite recuperar da primeira fila a columna indicada. Se non poñemos ningunha columna recuperar a primeira.
−
:* '''Paso 2: Chamar ao procedemento query enviando como parámetro a consulta a realizar e a conexión aberta previamente.'''
+
: Por exemplo, se realmente necesitamos coñecer o número de rexistros, realizaremos unha consulta específica empregando COUNT(*) e como sabemos que vai devolver unha fila cunha columna podemos empregar fetchColumn:
  
−
::* Versión procedimental:
+
::<syntaxhighlight lang="php">
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
$stmt = $conex->query(
−
$result=mysqli_query($conex, 'SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
+
'SELECT COUNT(*)
−
</syntaxhighlight>  
+
FROM libros'
−
::<u>Nota:</u> $conex é a conexión aberta feita no paso anterior.
+
);
 +
$numLibros = $stmt->fetchColumn();
 +
</syntaxhighlight>
  
−
::* Versión orientada a obxectos e PDO:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
 
−
</syntaxhighlight>
 
  
−
::* $result vai ter o conxunto de filas que devolva o resultado. Podemos velo como unha táboa formada polas filas do resultado e polas columnas indicadas no select.
 
  
  
  
 
<br />
 
<br />
−
:* '''Paso 3: Comprobar se temos datos ou se houbo algún tipo de erro na chamada.'''
+
::* Versión orientada a obxectos:
  
  
 
::* Se estamos a <u>empregar unha versión PHP superior ou igual á 8.1</u>, os erros deben ser capturados con try-catch:
 
::* Se estamos a <u>empregar unha versión PHP superior ou igual á 8.1</u>, os erros deben ser capturados con try-catch:
 
:::* Versión orientada a obxectos:
 
:::* Versión orientada a obxectos:
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="38-43" >
+
::::<syntaxhighlight lang="php" enclose="div" highlight="38-43" >
 
<?php
 
<?php
 
/* Lembrar quitar isto ao probar o scripts sen erros */
 
/* Lembrar quitar isto ao probar o scripts sen erros */
Línea 118: Línea 249:
  
  
 +
<br />
 +
::* Como vemos, en caso de erro comprobamos se o xestor devolveu un código de erro, indicativo de que a sentenza SQL está mal feita. A información do código de erro non debe amosarse a un usuario final. Normalmente, se pon o echo de 'Non hai datos que amosar' e se estamos a desenvolver a aplicación e non devolve datos, faríamos un echo temporal do código de erro do xestor ou un echo da sentenza SQL, podendo executala no xestor Mysql para ver o erro.
  
−
<br />
 
−
:::* Versión PDO:
 
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="30-34" >
 
−
<?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);
 
  
−
try {
 
−
    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
 
−
    $usuario = 'user_php';
 
−
    $password = 'user_php';
 
  
−
    $conex = new PDO($dsn, $usuario, $password);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
?>
 
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<br />
−
    "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>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
=== Recuperación de filas ===
−
    <?php       
+
'''Paso 4: Agora necesitaremos percorrer dende o primeiro rexistro ata o último amosando o resultado.'''
−
       
 
−
        $result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
 
−
        // Comprobar se hai filas
 
−
        if ($result && $result->rowCount() > 0) {
 
−
          // Procesamos os datos
 
−
        } else {
 
−
          echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
     
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
* O método fetch() recupera '''unha única fila''' do conxunto de resultados e avanza á seguinte.
−
}    // Pecha o try do principio da páxina
+
::<syntaxhighlight lang="php">
−
catch (PDOException $e) {
+
while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) {
−
  die("Erro PDO: " . $e->getMessage());
+
echo htmlspecialchars(
−
} finally {
+
    $fila['titulo'],
−
  $conex = null; // Pechar a conexión
+
    ENT_QUOTES | ENT_SUBSTITUTE,
 +
    'UTF-8'
 +
);
 
}
 
}
 +
</syntaxhighlight>
 +
: Fixarse que neste exemplo no facemos un if inicial có primeiro fetch para saber si hai datos ou non. Dependerá de vos como programadores determinar se é necesario para informar ao usuario o facer algún tipo de proceso nese caso.
  
−
?>
 
−
</syntaxhighlight>
 
  
 +
* Se na conexión establecemos:
 +
::<syntaxhighlight lang="php">
 +
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
 +
</syntaxhighlight>
 +
: non será necesario indicar PDO::FETCH_ASSOC en cada chamada:
 +
::<syntaxhighlight lang="php">
 +
while ($fila = $stmt->fetch()) {
 +
echo $fila['titulo'];
 +
}
 +
</syntaxhighlight>
 +
: PDO::FETCH_ASSOC o que fai é que Php soamente cre un array asociativo cós resultados. Se non o poñemos, devolve dous arrays, un no que se accede aos datos polo índice ($fila[0]) e outro asociativo por nome de columna ($fila['titulo']).
  
  
−
 
+
* '''Non debemos chamar dúas veces seguidas a fetch() para procesar o mesmo conxunto de resultados'''.
−
<br />
+
: Por exemplo, isto é incorrecto:
−
 
+
::<syntaxhighlight lang="php">
−
::* Se estamos a <u>empregar unha versión PHP inferior á 8.1</u> [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Visualizaci.C3.B3n_de_errores como xa comentara anteriormente], e a consulta está mal (por exemplo, seleccionamos unha táboa que non existe) $result <u>devolverá false</u> e podemos controlar os erros así:
+
while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
−
:::* Versión procedimental:
+
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="1,5-16" >
+
    // ...
−
if ($result && (mysqli_num_rows($result)>0)){
 
−
  // Veñen datos
 
−
 
 
 
}
 
}
−
else {
 
−
    if(mysqli_errno($conex)){
 
−
      echo "Houbo un erro na chamada ao xestor...";
 
−
    // O seguinte non tería que amosarse ao usuario final da aplicación. Posto para desenvolvemento.
 
−
      echo "<div>";
 
−
      printf("Erro no Mysql. Número %d. Mensaxe: %s",  mysqli_errno($conex),  mysqli_error($conex));
 
−
      echo "<div>";
 
−
    }
 
−
    else{
 
−
      echo "Non hai datos que amosar!!!!";
 
−
    }
 
 
}
 
}
−
</syntaxhighlight>  
+
</syntaxhighlight>
−
 
 
−
 
 
−
:::* Versión orientada a obxectos:
 
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="1,5-16" >
 
−
if ($result && ($result->num_rows>0)){
 
−
  // Veñen datos
 
  
 +
: O primeiro fetch() xa consume unha fila e o segundo bucle comezaría na seguinte.
 +
: A forma correcta sería:
 +
::<syntaxhighlight lang="php">
 +
while ($row = $stmt->fetch()) {
 +
// Procesamos a fila
 
}
 
}
−
else {
+
</syntaxhighlight>
−
    if($conex->errno){
 
−
      echo "Houbo un erro na chamada ao xestor...";
 
−
    // O seguinte non tería que amosarse ao usuario final da aplicación. Posto para desenvolvemento.
 
−
      echo "<div>";
 
−
      printf("Erro no Mysql. Número %d. Mensaxe: %s",  $conex->errno,  $conex->error);
 
−
      echo "<div>";
 
−
    }
 
−
    else{
 
−
      echo "Non hai datos que amosar!!!!";
 
−
    }
 
−
}
 
−
</syntaxhighlight>  
 
−
 
 
  
  
Línea 225: Línea 310:
  
 
<br />
 
<br />
−
::* Como vemos, en caso de erro comprobamos se o xestor devolveu un código de erro, indicativo de que a sentenza SQL está mal feita. A información do código de erro non debe amosarse a un usuario final. Normalmente, se pon o echo de 'Non hai datos que amosar' e se estamos a desenvolver a aplicación e non devolve datos, faríamos un echo temporal do código de erro do xestor ou un echo da sentenza SQL, podendo executala no xestor Mysql para ver o erro.
+
:* Versión procedimental
−
 
 
−
 
 
−
 
 
−
 
 
  
−
<br />
 
−
:* '''Paso 4: Agora necesitaremos percorrer dende o primeiro rexistro ata o último amosando o resultado.'''
 
−
::: Isto o faremos cun bucle, chamando á [http://php.net/manual/es/mysqli-result.fetch-assoc.php función mysqli_result::fetch_assoc(void)].
 
 
::: Debemos entender que, inicialmente, despois de facer a consulta, temos un punteiro que está 'apuntando' xusto antes do primeiro rexistro de resultados. O que fai a función anterior é mover ese punteiro ao seguinte rexistro (a primeira vez apuntará ao primeiro do conxunto de resultados) e devolve nun array a fila á que está 'apuntando'.  
 
::: Debemos entender que, inicialmente, despois de facer a consulta, temos un punteiro que está 'apuntando' xusto antes do primeiro rexistro de resultados. O que fai a función anterior é mover ese punteiro ao seguinte rexistro (a primeira vez apuntará ao primeiro do conxunto de resultados) e devolve nun array a fila á que está 'apuntando'.  
 
::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false.
 
::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false.
  
  
 +
::: Isto o faremos cun bucle, chamando á [http://php.net/manual/es/mysqli-result.fetch-assoc.php función mysqli_result::fetch_assoc(void)].
 
::* Versión procedimental:
 
::* Versión procedimental:
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="3-5" >
+
::::<syntaxhighlight lang="php" enclose="div" highlight="3-5" >
 
if ($result && (mysqli_num_rows($resullt)>0)){
 
if ($result && (mysqli_num_rows($resullt)>0)){
 
   // Veñen datos
 
   // Veñen datos
Línea 253: Línea 332:
  
 
::* Versión orientada a obxectos:
 
::* Versión orientada a obxectos:
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="3-5" >
+
::::<syntaxhighlight lang="php" enclose="div" highlight="3-5" >
 
if ($result && ($result->num_rows>0)){
 
if ($result && ($result->num_rows>0)){
 
   // Veñen datos
 
   // Veñen datos
Línea 267: Línea 346:
  
 
::* Versión orientada a obxectos con fetch_object:
 
::* Versión orientada a obxectos con fetch_object:
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="3,4" >
+
:::<syntaxhighlight lang="php" enclose="div" highlight="3,4" >
 
if ($result && ($result->num_rows>0)){
 
if ($result && ($result->num_rows>0)){
 
   // Veñen datos
 
   // Veñen datos
Línea 277: Línea 356:
 
   echo 'Non hai datos ou a consulta está mal feita';
 
   echo 'Non hai datos ou a consulta está mal feita';
 
}
 
}
−
</syntaxhighlight>  
+
</syntaxhighlight>
 +
 
 +
=== Saída de datos en HTML ===
 +
Para amosar datos dentro dunha páxina HTML empregaremos normalmente htmlspecialchars():
 +
::<syntaxhighlight lang="php">
 +
echo htmlspecialchars(
 +
$fila['titulo'],
 +
ENT_QUOTES | ENT_SUBSTITUTE,
 +
'UTF-8'
 +
);
 +
</syntaxhighlight>
 +
Non debemos empregar urlencode() para codificar texto que simplemente se vai amosar ao usuario.
 +
: Por exemplo, para crear un enlace a un autor:
 +
::<syntaxhighlight lang="php">
 +
printf(
 +
"<a href='detalle_autor.php?id=%d'>%s, %s</a>",
 +
(int) $row['id'],
 +
htmlspecialchars(
 +
$row['apelidos'],
 +
ENT_QUOTES | ENT_SUBSTITUTE,
 +
'UTF-8'
 +
),
 +
htmlspecialchars(
 +
$row['nome'],
 +
ENT_QUOTES | ENT_SUBSTITUTE,
 +
'UTF-8'
 +
)
 +
);
 +
</syntaxhighlight>
 +
: urlencode() ou rawurlencode() utilizaranse cando sexa necesario codificar datos que forman parte dunha URL.
 +
=== Consultas con parámetros numéricos ===
 +
Podemos empregar parámetros con nome:
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTA_CON_PARAM_NUMERICO',
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor'
 +
);
 +
</syntaxhighlight>
 +
Preparamos e executamos a consulta:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTA_CON_PARAM_NUMERICO
 +
);
 +
$stmt->execute([
 +
'valor' => 5
 +
]);
 +
</syntaxhighlight>
 +
: '''IMPORTANTE:''' O nome do parámetro debe coincidir co definido na consulta.
 +
: Se na consulta temos:
 +
::<syntaxhighlight lang="sql">
 +
WHERE col1 = :valor
 +
</syntaxhighlight>
 +
: debemos empregar:
 +
::<syntaxhighlight lang="php">
 +
'valor' => 5
 +
</syntaxhighlight>
 +
: e non outro nome como valor1.
 +
 
 +
=== Tipo dos parámetros ===
 +
Cando enviamos directamente un array ao método execute():
 +
::<syntaxhighlight lang="php">
 +
$stmt->execute([
 +
'valor' => 5
 +
]);
 +
</syntaxhighlight>
 +
: PDO trata os valores do array como PDO::PARAM_STR.
 +
: O xestor de base de datos pode realizar posteriormente conversións de tipo segundo o contexto da consulta.
 +
Na maioría das consultas sinxelas isto non supón un problema.
 +
Se queremos indicar explicitamente o tipo do parámetro podemos empregar bindValue():
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor'
 +
);
 +
$stmt->bindValue(
 +
':valor',
 +
5,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
Algúns tipos habituais son:
 +
::<syntaxhighlight lang="text">
 +
PDO::PARAM_INT
 +
PDO::PARAM_STR
 +
PDO::PARAM_BOOL
 +
PDO::PARAM_NULL
 +
</syntaxhighlight>
 +
 
 +
 
 +
* É especialmente recomendable indicar explicitamente o tipo cando este sexa relevante para a consulta.
 +
: Por exemplo, nos parámetros empregados en LIMIT ou OFFSET:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT :inicio,:cantidade'
 +
);
 +
$stmt->bindValue(
 +
':inicio',
 +
$inicio,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->bindValue(
 +
':cantidade',
 +
$cantidade,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->execute();
 +
</syntaxhighlight>
  
 +
=== Parámetros de tipo cadea ===
 +
Para enviar unha cadea empregaremos tamén unha sentenza preparada:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id, apelidos, nome
 +
FROM autores
 +
WHERE apelidos = :apelidos'
 +
);
 +
$stmt->execute([
 +
'apelidos' => $apelidos
 +
]);
 +
</syntaxhighlight>
  
  
 +
* Se queremos facer unha busca mediante LIKE, o marcador debe representar o '''valor completo''':
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id, apelidos, nome
 +
FROM autores
 +
WHERE apelidos LIKE :apelidos'
 +
);
 +
$stmt->execute([
 +
'apelidos' => '%' . $apelidos . '%'
 +
]);
 +
</syntaxhighlight>
  
−
::* Versión PDO:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="3-5" >
 
−
        $stmt = $conex->query('SELECT col1, col2 FROM TABOA_1');
 
−
        // Comprobar se hai filas
 
−
        if ($result && $result->rowCount() > 0) {
 
−
          // Procesamos os datos
 
−
          while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) { // tamén pode ser PDO::FETCH_OBJ
 
−
              echo htmlentities($fila['col1']) . ' - ' . htmlentities($fila['col2']) . "<br>";
 
−
          }
 
−
        } else {
 
−
          echo "Non hai datos que amosar!!!!";
 
−
        }
 
  
−
</syntaxhighlight>  
+
* Non debemos escribir:
 +
::<syntaxhighlight lang="sql">
 +
WHERE apelidos LIKE '%:apelidos%'
 +
</syntaxhighlight>
 +
: xa que o marcador debe ocupar o lugar dun valor completo (PHP/PDO interpretará :apelidos como texto literal dentro das comiñas e non o substituirá pola variable).
  
−
::: Se temos poucas filas podemos obter todas as filas de vez empregando [https://www.php.net/manual/es/pdostatement.fetchall.php fetchall()].
+
=== Validación dos parámetros recibidos ===
 +
As sentenzas preparadas evitan que os valores enviados como parámetros sexan interpretados como parte da instrución SQL, pero '''non substitúen a validación dos datos'''.
 +
Por exemplo, se esperamos recibir un identificador numérico desde un formulario:
 +
::<syntaxhighlight lang="php">
 +
$libroId = filter_input(
 +
INPUT_POST,
 +
'lstLibros',
 +
FILTER_VALIDATE_INT
 +
);
 +
if (
 +
$libroId !== false &&
 +
$libroId !== null &&
 +
$libroId !== -1
 +
) {
 +
$stmt = $conex->prepare(
 +
    CONSULTAR_EXEMPLARES_POR_LIBRO
 +
);
  
 +
$stmt->bindValue(
 +
    ':libro_id',
 +
    $libroId,
 +
    PDO::PARAM_INT
 +
);
  
−
<br />
+
$stmt->execute();
−
:* '''Paso 5: Ao rematar de percorrer o resultado, liberaremos a memoria chamando á [http://php.net/manual/es/mysqli-result.free.php función free()]'''
 
  
−
::* Versión procedimental:
+
// Procesamos os resultados
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="6" >
 
−
if ($result && (mysqli_num_rows($result)>0)){
 
−
  // Veñen datos
 
−
  while($row = mysqli_fetch_assoc($result)) {
 
−
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
 
−
  }
 
−
  mysqli_free_result($result);  // Liberamos da memoria os recursos
 
 
}
 
}
−
else {
+
</syntaxhighlight>
−
  echo 'Non hai datos ou a consulta está mal feita';
+
=== Caso práctico: consultar exemplares dun libro ===
−
}
+
'''Arquivo bd.php'''
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
 +
 
 +
define('CARTAFOL_IMAXES_WEB', 'http://localhost/imaxes/');
 +
 
 +
define(
 +
'CONSULTAR_EXEMPLARES_POR_LIBRO',
 +
'SELECT libro_id,
 +
id_exemplar,
 +
imaxe,
 +
prezo,
 +
estado_id
 +
FROM exemplares
 +
WHERE libro_id = :libro_id
 +
ORDER BY estado_id'
 +
);
 +
</syntaxhighlight>
  
  
−
::* Versión orientada a obxectos con fetch_assoc:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="6" >
 
−
if ($result && ($result->num_rows>0)){
 
−
  // Veñen datos
 
−
  while($row = $result->fetch_assoc()) {
 
−
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
 
−
  }
 
−
  $result->free();  // Liberamos da memoria os recursos
 
−
}
 
−
else {
 
−
  echo 'Non hai datos ou a consulta está mal feita';
 
−
}
 
−
</syntaxhighlight>
 
  
 +
<br />
 +
* '''Código PHP'''
 +
::<syntaxhighlight lang="php">
 +
<html>
  
−
::* Versión PDO:
+
<head>Páxina de exemplo
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="7" >
+
</head>
−
// Comprobar se hai filas
 
−
if ($result && $result->rowCount() > 0) {
 
−
  // Procesamos os datos
 
−
  while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) { // tamén pode ser PDO::FETCH_OBJ
 
−
        echo htmlentities($fila['col1']) . ' - ' . htmlentities($fila['col2']) . "<br>";
 
−
  }
 
−
  $result = null;
 
−
} else {
 
−
  echo "Non hai datos que amosar!!!!";
 
−
}
 
  
 +
<body>
  
−
</syntaxhighlight>
+
    <?php
 +
    ini_set('display_errors', 1);
 +
    ini_set('display_startup_errors', 1);
 +
    error_reporting(E_ALL);
  
 +
    require('/var/www/confBD/bd.php');
 +
    try {
  
 +
        $dsn = sprintf(
 +
            'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
            SERVIDOR,
 +
            BD
 +
        );
  
 +
        $opciones = [
 +
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_EMULATE_PREPARES  => false
 +
        ];
  
−
<br />
+
        $conex = new PDO(
−
:* '''Paso 6: Pechamos a conexión.'''.
+
            $dsn,
 +
            USUARIO,
 +
            PASSWORD,
 +
            $opciones
 +
        );
  
−
:: [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Peche_da_conexi.C3.B3n Pechamos a conexión como xa vimos nunha sección anterior.]
+
        $libroId = filter_input(INPUT_POST,'lstLibros',FILTER_VALIDATE_INT);  // Como non temos feito a páxina de formulario na seguinte liña dou un valor específico...
 +
        $libroId = 1;  // Este dato virá dunha caixa de texto dunha páxina anterior que terá como action do formulario esta páxina (pode facerse todo nunha mesma páxina)
  
−
=== Caso Práctico 1 ===
+
        if (
−
: Imos crear unha consulta que nos devolva todos os libros da táboa LIBROS dentro dunha táboa:
+
            $libroId !== false &&
−
: Para non ter as consultas no medio do código PHP das diferentes páxinas, imos definila no '''arquivo bd.inc.php''' creado no punto anterior.
+
            $libroId !== null &&
 +
            $libroId !== -1
 +
        ) {
 +
            $stmt = $conex->prepare(
 +
                CONSULTAR_EXEMPLARES_POR_LIBRO
 +
            );
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
            $stmt->bindValue(
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
+
                ':libro_id',
−
</syntaxhighlight>
+
                $libroId,
 +
                PDO::PARAM_INT
 +
            );
  
 +
            $stmt->execute();
  
−
* '''Normas nos nomes das constantes''':
+
            $fila = $stmt->fetch();
−
:* Comenzarán cun verbo que vai poder ser: CONSULTAR, INSERIR, MODIFICAR e ELIMINAR
 
−
:* Seguirá cun guión baixo.
 
−
:* En caso de consultar os datos dunha única táboa se indicará o nome da táboa. En caso de consultar datos de varias táboas se indicará con varias palabras separadas con guión baixo que indiquen o obxectivo da consulta.
 
  
 +
            if ($fila !== false) {
  
 +
                echo "<table border='1'>";
 +
                echo '<tr>';
 +
                echo '<th>Num. exemplar</th>';
 +
                echo '<th>Imaxe</th>';
 +
                echo '<th>Prezo</th>';
 +
                echo '<th>Estado</th>';
 +
                echo '</tr>';
  
 +
                do {
  
−
* Aplicado ao exemplo que estamos a seguir...
+
                    echo '<tr>';
−
:<u>Nota</u> Lembrar activar as directivas para amosar erros e quitalas cando rematedes a páxina.
 
  
−
==== Solución Caso Práctico 1 ====
+
                    printf(
 +
                        '<td>%d</td>',
 +
                        (int) $fila['id_exemplar']
 +
                    );
  
−
'''Arquivo: consultar_libros.php'''
+
                    printf(
−
::<syntaxhighlight lang="java" enclose="div" highlight="27-40" >
+
                        "<td><img alt='%s' src='%s' width='42' height='42'></td>",
−
<?php
+
                        htmlspecialchars(
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
                            $fila['imaxe'] ?? '',
−
ini_set('display_errors', 1);
+
                            ENT_QUOTES | ENT_SUBSTITUTE,
−
ini_set('display_startup_errors', 1);
+
                            'UTF-8'
−
error_reporting(E_ALL);
+
                        ),
 +
                        htmlspecialchars(
 +
                            CARTAFOL_IMAXES_WEB . ($fila['imaxe'] ?? ''),
 +
                            ENT_QUOTES | ENT_SUBSTITUTE,
 +
                            'UTF-8'
 +
                        )
 +
                    );
  
−
require ('/var/www/confBD/bd.inc.php');
+
                    printf(
 +
                        '<td>%.2f</td>',
 +
                        (float) ($fila['prezo'] ?? 0)
 +
                    );
  
−
try {
+
                    printf(
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD);  // Non indicamos o nome da bd
+
                        '<td>%d</td>',
−
    $conex->set_charset('utf8mb4');
+
                        (int) $fila['estado_id']
−
    $conex->select_db(BD); // O parámetro indica o nome da base de datos
+
                    );
  
−
?>
+
                    echo '</tr>';
 +
                } while ($fila = $stmt->fetch());
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
                echo '</table>';
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
            } else {
−
<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>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
                 echo 'Non hai datos que amosar';
−
    <?php       
 
−
       
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->num_rows>0){
 
−
            echo "<table border='1'>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                echo '<tr>';
 
−
                printf("<td>%s</td>",htmlentities($row["titulo"]));
 
−
                 echo '</tr>';
 
 
             }
 
             }
−
            echo '</table>';
 
−
            $result->free();  // Liberamos da memoria os recursos
 
 
         }
 
         }
−
         else {
+
    } catch (PDOException $e) {
−
            echo "Non hai datos que amosar!!!!";
+
         die("Error PDO: " . $e->getMessage());
−
        }
+
    } catch (Throwable $e) {
−
       
+
        die("Error general: " . $e->getMessage());
−
     ?>      
+
    }
−
    </body>  
+
 
 +
    $conex = null;  // Poderíamos non poñer nada
 +
     ?>
 +
</body>
 +
 
 
</html>
 
</html>
 +
</syntaxhighlight>
  
−
<?php
+
== Procedementos almacenados empregando PDO ==
−
}    // Pecha o try do principio da páxina
+
Os procedementos almacenados execútanse mediante a sentenza SQL CALL.
−
catch (mysqli_sql_exception $e) {
+
: Por exemplo:
−
     die("Erro mysql: " . $e->getMessage());
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_LIBROS()'
 +
);
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
Se o procedemento devolve un conxunto de resultados podemos percorrelo da forma habitual:
 +
::<syntaxhighlight lang="php">
 +
while ($fila = $stmt->fetch()) {
 +
echo htmlspecialchars(
 +
     $fila['titulo'],
 +
    ENT_QUOTES | ENT_SUBSTITUTE,
 +
    'UTF-8'
 +
);
 
}
 
}
−
catch (Expcetion $e) {
+
</syntaxhighlight>
−
    die("Erro xeral: " . $e->getMessage());
+
=== Procedementos con varios conxuntos de resultados ===
 +
Un procedemento almacenado pode devolver máis dun conxunto de resultados.
 +
Para pasar ao seguinte conxunto empregaremos:
 +
::<syntaxhighlight lang="php">
 +
nextRowset()
 +
</syntaxhighlight>
 +
: Por exemplo:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL PROCEDEMENTO_CON_VARIOS_SELECT()'
 +
);
 +
$stmt->execute();
 +
do {
 +
while ($fila = $stmt->fetch()) {
 +
 
 +
    // Procesar as filas deste conxunto
 
}
 
}
−
finally{
+
} while ($stmt->nextRowset());
−
  if(!empty($conex)){
+
</syntaxhighlight>
−
      $conex->close();
+
 
−
  }
 
−
}
 
−
?>
 
  
 +
+ Antes de executar outra consulta na mesma conexión debemos asegurarnos de que non queden resultados pendentes.
 +
: Podemos liberar os resultados restantes mediante:
 +
::<syntaxhighlight lang="php">
 +
$stmt->closeCursor();
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
: Isto é especialmente importante despois de chamadas a procedementos almacenados en MySQL.
  
  
−
* Resultado:
+
: Por exemplo:
−
[[Imagen:Php_BD_8.jpg|300px|center]]
+
::<syntaxhighlight lang="php">
 +
<?php
 +
// 1. Executamos o procedemento almacenado que devolve varios SELECTs
 +
$stmt = $conex->prepare('CALL PROCEDEMENTO_CON_VARIOS_SELECT()');
 +
$stmt->execute();
  
 +
// 2. Procesamos só o primeiro conxunto de resultados (por exemplo, os primeiros 3 datos)
 +
if ($fila = $stmt->fetch()) {
 +
    echo "Primeiro rexistro: " . $fila['nome'] . "<br>";
 +
}
  
−
* Fixarse como neste exemplo, aínda que a consulta devolve o id do libro, como non o necesitamos non o amosamos ao usuario, xa que dito dato é un código interno que utilizamos como clave primaria.
+
// 3. Pechamos o cursor para liberar a conexión
 +
// Isto descarta calquera outra fila ou segundo resultset pendente
 +
$stmt->closeCursor();
  
 +
// 4. Agora xa podemos executar outra consulta na mesma conexión sen erros
 +
$outraConsulta = $conex->query('SELECT * FROM outra_taboa');
 +
$datos = $outraConsulta->fetchAll();
 +
?>
  
 +
</syntaxhighlight>
 +
 +
=== Procedementos con parámetros de entrada ===
 +
Os parámetros de entrada enviaranse empregando marcadores:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL PROCEDEMENTO_CON_PARAMETROS(
 +
:dato1,:dato2
  
−
* O método fetch_assoc() move un punteiro interno á seguinte fila de resultados.
+
)'
−
: Se queremos modificar dito punteiro e situarnos en calquera fila do resultado, debemos facer uso da [http://php.net/manual/es/mysqli-result.data-seek.php función mysqli_data_seek]·
+
);
 +
$stmt->bindValue(
 +
':dato1',
 +
$valorNum,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->bindValue(
 +
':dato2',
 +
$valorTexto,
 +
PDO::PARAM_STR
 +
);
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
: Desta forma non necesitamos construír a chamada mediante sprintf() nin concatenar os valores dentro da sentenza SQL.
  
 +
=== Procedementos con parámetros de saída en MySQL ===
 +
PDO define mecanismos xenéricos para parámetros de saída, pero o controlador '''PDO_MYSQL non xestiona correctamente PDO::PARAM_INPUT_OUTPUT'''.
 +
Por este motivo, con MySQL podemos empregar unha variable de usuario de MySQL para recuperar un parámetro OUT.
 +
Supoñamos este procedemento:
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
 +
CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
 +
OUT p_num_libros INT
 +
)
 +
BEGIN
 +
SELECT COUNT(*)
 +
INTO p_num_libros
 +
FROM libros;
 +
END$$
 +
DELIMITER ;
 +
</syntaxhighlight>
 +
Desde PHP podemos facer:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_NUM_LIBROS(
 +
@numero_libros
 +
)'
 +
);
 +
$stmt->execute();
 +
/*
 +
Liberamos todos os resultados da chamada
 +
antes de executar outra sentenza SQL.
 +
*/
 +
$stmt->closeCursor();
 +
$stmt = $conex->query(
 +
'SELECT @numero_libros'
 +
);
 +
$numLibros = $stmt->fetchColumn();
 +
printf(
 +
'<h1>Número de libros: %d</h1>',
 +
(int) $numLibros
 +
);
 +
</syntaxhighlight>
 +
: É importante chamar a closeCursor() antes do segundo SELECT, xa que MySQL pode manter conxuntos de resultados pendentes da chamada ao procedemento.
 +
== Funcións almacenadas de MySQL ==
 +
As funcións de MySQL poden empregarse dentro dunha instrución SELECT.
 +
Se a función non leva parámetros podemos facer:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT CONSULTAR_NUM_LIBROS()'
 +
);
 +
$numLibros = $stmt->fetchColumn();
 +
</syntaxhighlight>
 +
Se leva parámetros empregaremos unha sentenza preparada:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT Suma(:dato1,:dato2 )'
 +
);
 +
$stmt->bindValue(
 +
':dato1',
 +
5,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->bindValue(
 +
'':dato2,
 +
7,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->execute();
 +
$resultado = $stmt->fetchColumn();
 +
</syntaxhighlight>
 +
: fetchColumn() resulta especialmente cómodo cando a consulta devolve un único valor.
  
 +
== Paxinación de resultados empregando PDO ==
 +
Para paxinar resultados empregaremos:
 +
:* COUNT(*) para coñecer o número total de rexistros.
 +
:* LIMIT para indicar a cantidade de rexistros que queremos recuperar.
 +
:* OFFSET, ou o primeiro parámetro de LIMIT, para indicar desde que rexistro queremos comezar.
 +
Por exemplo:
 +
::<syntaxhighlight lang="sql">
 +
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT 10 OFFSET 20
 +
</syntaxhighlight>
 +
: Esta consulta devolvería ata 10 rexistros empezando desde o rexistro situado na posición 20.
  
  
  
 
<br />
 
<br />
 +
'''Número total de rexistros'''
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
 +
$numLibros = (int) $stmt->fetchColumn();
 +
</syntaxhighlight>
  
−
=== Caso Práctico 2 ===
 
  
−
* Agora imos facer unha consulta á táboa AUTORES recuperando todos os autores ordenados por apelidos e os amosaremos en forma de lista desordenada no que cada elemento da lista será enlace có seguinte formato: <enlace href='detalle_autor.php?id=XXX'>Apelidos , nome </enlace>
 
−
: Sendo XXX a columna id_autor.
 
  
−
: <u>Nota:</u> Se queremos facer máis seguro o paso de información entre páxinas, poderíamos empregar un formulario e que cada autor tivera un button con un value igual á clave primaria. De tal forma que ao premer nel, o formulario envaría á seguinte páxina o id do autor premido e dita información non ser vería na URL. Por exemplo: <button type="submit" name="enviar" value="valor_a_enviar">Texto a amosar</button>
+
<br />
 +
'''Páxina actual'''
 +
O número de páxina recibido mediante GET é un dato externo e debe validarse.
 +
::<syntaxhighlight lang="php">
 +
$paxinaActual = filter_input(
 +
INPUT_GET,
 +
'paxina',
 +
FILTER_VALIDATE_INT
 +
);
 +
if (
 +
$paxinaActual === false ||
 +
$paxinaActual === null ||
 +
$paxinaActual < 1
 +
) {
 +
$paxinaActual = 1;
 +
}
 +
</syntaxhighlight>
 +
: htmlspecialchars() non serve para validar que un dato sexa un número. Empregarase posteriormente cando teñamos que inserir unha cadea dentro de HTML.
  
  
 +
<br />
 +
'''Cálculo do número de páxinas'''
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONST_NUM_LIBROS_PAXINA',
 +
10
 +
);
 +
$totalPaxinas = (int) ceil(
 +
$numLibros / CONST_NUM_LIBROS_PAXINA
 +
);
 +
</syntaxhighlight>
 +
Podemos evitar que se solicite unha páxina superior á existente:
 +
::<syntaxhighlight lang="php">
 +
if (
 +
$totalPaxinas > 0 &&
 +
$paxinaActual > $totalPaxinas
 +
) {
 +
$paxinaActual = $totalPaxinas;
 +
}
 +
</syntaxhighlight>
  
  
 
<br />
 
<br />
−
==== Solución Caso Práctico 2 ====
+
'''Calcular o desprazamento'''
−
 
+
::<syntaxhighlight lang="php">
−
: No '''arquivo bd.inc.php''' creado no punto anterior.
+
$inicio = (
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
$paxinaActual - 1
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
+
) * CONST_NUM_LIBROS_PAXINA;
−
</syntaxhighlight>  
+
</syntaxhighlight>
  
 +
: Por exemplo, se estamos:
 +
:* Na páxina 1 => inicio = 0
 +
:* Na páxina 2 => inicio = 10
 +
:* Na páxina 3 => inicio = 20
  
  
 
<br />
 
<br />
−
'''Páxina consultar_autores.php'''
+
'''Consulta paxinada'''
−
:<syntaxhighlight lang="java" enclose="div" highlight="26-40" >
+
Neste caso indicaremos explicitamente que os parámetros son enteiros:
−
<?php
+
::<syntaxhighlight lang="php">
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
$stmt = $conex->prepare(
−
ini_set('display_errors', 1);
+
'SELECT id, titulo
−
ini_set('display_startup_errors', 1);
+
FROM libros
−
error_reporting(E_ALL);
+
ORDER BY titulo
 +
LIMIT :limite
 +
OFFSET :desprazamento'
 +
);
 +
$stmt->bindValue(
 +
':limite',
 +
CONST_NUM_LIBROS_PAXINA,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->bindValue(
 +
':desprazamento',
 +
$inicio,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->execute();
 +
$libros = $stmt->fetchAll();
 +
</syntaxhighlight>
  
−
require ('/var/www/confBD/bd.inc.php');
 
  
−
try {
+
* Despois podemos amosar os libros:
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);    // Indico nome da bd
+
::<syntaxhighlight lang="php">
−
    $conex->set_charset('utf8mb4');
+
if ($libros) {
 +
echo "<table border='1'>";
  
−
?>
+
foreach ($libros as $libro) {
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
     echo '<tr>';
−
     "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>Operacións BD</title>
 
−
    </head>
 
  
−
     <body>
+
     printf(
−
    <?php       
+
         '<td>%s</td>',
−
       
+
        htmlspecialchars(
−
        $result=$conex->query(CONSULTAR_AUTORES);
+
            $libro['titulo'],
−
         if ($result && $result->num_rows>0){
+
             ENT_QUOTES | ENT_SUBSTITUTE,
−
            // Veñen datos
+
             'UTF-8'
−
            echo "<ul>";
+
         )
−
            while($row = mysqli_fetch_assoc($result)) {
+
    );
−
                echo "<li>";
 
−
                printf("<a href='detalle_autor.php?id=%s'>%s</a>",$row["id"],urlencode($row["apelidos"]) .', '. urlencode($row['nome']));
 
−
                echo "</li>";
 
−
             }
 
−
             echo "</ul>";
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
         }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
    echo '</tr>';
 
}
 
}
−
catch (mysqli_sql_exception $e) {
+
 
−
    die("Erro mysql: " . $e->getMessage());
+
echo '</table>';
 +
} else {
 +
echo 'Non hai datos que amosar';
 
}
 
}
−
catch (Expcetion $e) {
+
</syntaxhighlight>
−
     die("Erro xeral: " . $e->getMessage());
+
 
 +
 
 +
 
 +
 
 +
<br />
 +
'''Enlaces da paxinación'''
 +
::<syntaxhighlight lang="php">
 +
echo '<div>';
 +
for (
 +
$cont = 1;
 +
$cont <= $totalPaxinas;
 +
$cont++
 +
) {
 +
if ($cont === $paxinaActual) {
 +
 
 +
     printf(
 +
        '<strong>%d</strong> ',
 +
        $cont
 +
    );
 +
 
 +
} else {
 +
 
 +
    printf(
 +
        "<a href='?paxina=%d'>%d</a> ",
 +
        $cont,
 +
        $cont
 +
    );
 
}
 
}
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
 
}
 
}
−
?>
+
echo '</div>';
 +
</syntaxhighlight>
 +
 
 +
 
 +
 
 +
<br />
 +
'''Sobre a optimización do número total de rexistros'''
 +
En xeral non deberiamos enviar o número total de rexistros na URL confiando despois nese valor:
 +
::<syntaxhighlight lang="text">
 +
?paxina=2&numtotal=50
 +
</syntaxhighlight>
 +
: Os datos enviados mediante GET poden ser modificados polo usuario.
 +
Tampouco debemos asumir que gardar o número total nunha variable de sesión é sempre unha optimización correcta, xa que a base de datos pode cambiar e o valor almacenado quedar desactualizado.
 +
Como primeira solución, realizaremos en cada petición:
 +
::<syntaxhighlight lang="sql">
 +
SELECT COUNT(*)
 +
FROM libros
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
: Se nunha aplicación real esta operación chegase a ser un problema de rendemento, poderían estudarse posteriormente técnicas de caché ou outras estratexias de optimización.
 +
 +
== Xestión de excepcións ==
 +
 +
 +
* Nos exemplos completos empregaremos o seguinte esquema:
 +
::<syntaxhighlight lang="php">
 +
try {
 +
    $conex = new PDO($dsn, $usuario, $contrasinal, [
 +
        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 (monolog, etc.)
 +
    error_log($e->getMessage());
 +
   
 +
    die('Erro relacionado coa base de datos.');
 +
} catch (Throwable $e) {
 +
    die('Produciuse un erro na aplicación.');
 +
}
  
 +
// Non fai falta "finally {$conex = null;}":
 +
// Se o script continúa, destruirasie cando cumpra o seu ciclo;
 +
// se entra nos catch, o die() xa finaliza o proceso e libera a memoria.
 +
</syntaxhighlight>
 +
: PDOException permite tratar especificamente os erros de PDO.
 +
: Throwable permite capturar outros erros ou excepcións producidos durante a execución.
 +
: Os bloques catch deben colocarse desde o máis específico ao máis xeral.
  
  
  
 
<br />
 
<br />
 +
== Exercicios prácticos ==
  
−
=== Consulta enviando parámetros de tipo numérico ===
+
* Partimos de que xa temos creada a base de datos nunha [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operacións_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql sección anterior desta wiki] e gardados os datos necesarios para realizar a conexión no arquivo `bd.php`.
 +
 
 +
* '''IMPORTANTE:''' Nos exames, exercicios e prácticas avaliables empregaremos '''PDO''' para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario.
 +
 
 +
* Os exemplos con `mysqli` poden consultarse como información adicional, pero os exemplos de referencia dos apuntes serán os realizados mediante PDO.
 +
 
 +
* Nos exemplos completos empregaremos o seguinte esquema:
 +
 
 +
: No arquivo bd.php que se atopa fora da ruta /var/www/html gardaremos os datos da conexión e as consultar a empregar:
 +
::<syntaxhighlight lang="php">
 +
<?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 van acceder á base de datos terán este esquema:
 +
::<syntaxhighlight lang="php">
 +
try {
 +
 
 +
require '/var/www/confBD/bd.php';
 +
 
 +
try {
 +
 
 +
    $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
 +
        ]
 +
    );
  
−
<u>Nota:</u> Lembra que no paso de datos temos que ter en conta os posibles 'ataques' que poidan intentar facerse. Repasar o dado [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios na wiki anteriormente].
+
    // Operacións coa base de datos...
  
−
: Neste curso '''sempre''' que se vaia a enviar un dato ao xestor Mysql aplicarémoslle a [https://www.php.net/manual/es/mysqli.real-escape-string.php función mysqli_real_escape_string]. O ideal sería empregar procedementos almacenados como veremos posteriormente.
 
  
 +
} catch (PDOException $e) {
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
 
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
 
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col1='''%d'''
+
// Rexistrar o erro nun log.
 +
// Nunha aplicación real tamén poderían empregarse
 +
// ferramentas como Monolog.
 +
error_log($e->getMessage());
  
−
: No exemplo, col1 é de tipo numérico.
+
die('Erro relacionado coa base de datos.');
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1=%d');
 
  
−
$query = sprintf(CONSULTA_CON_PARAM_NUMERICO,valor_numérico_a_enviar);
+
} catch (Throwable $e) {
−
</syntaxhighlight>
 
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
die('Produciuse un erro na aplicación.');
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
......
 
−
$result=$conex->query(CONSULTA_CON_PARAM_NUMERICO);
 
−
......
 
−
</syntaxhighlight>
 
  
 +
} </syntaxhighlight>
  
 +
:* `PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC` fai que, por defecto, as filas recuperadas mediante `fetch()` ou `fetchAll()` sexan arrays asociativos.
 +
:* `PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION` fai que os erros producidos por PDO xeren unha excepción de tipo `PDOException`.
 +
:* `PDOException` permítenos tratar especificamente os erros relacionados con PDO.
 +
:* `Throwable` permite capturar outros erros ou excepcións producidos durante a execución da aplicación.
 +
:* Os bloques `catch` deben ordenarse desde o máis específico ao máis xeral.
 +
:* Nunha aplicación en produción non debemos amosar ao usuario a mensaxe devolta por `$e->getMessage()`. Podemos rexistrala mediante `error_log()` ou outro sistema de logs.
  
 
<br />
 
<br />
−
==== Caso práctico: Enviando parámetros de tipo numérico ====
 
  
−
* Seguindo con exemplo, imos facer que cando o usuario seleccione un libro dentro dunha combobox, aparezan os exemplares do mesmo.
+
=== Caso Práctico 1 ===
−
: Para iso teremos que:
+
 
−
:* Crear a sentencia SQL correspondente enviando un parámetro que vai ser o código do libro seleccionado.
+
* Imos crear unha consulta que devolva todos os libros da táboa `LIBROS` e os amose dentro dunha táboa HTML.
−
:* Obter dito dato. Isto o faremos utilizando un formulario cun método POST.
+
 
−
:* Modificar o código da páxina anterior (Consultar_Libros.php) e facer que os libros aparezan nunha combobox (dropdownlist ou <select> en html).
+
* Para non ter as consultas SQL no medio do código das diferentes páxinas, definirémolas no arquivo `bd.php`.
−
:* Facer que cando o usuario escolla un libro 'active' o formulario e polo tanto provoque o 'submit' do mesmo amosando os exemplares do libro seleccionado.
+
 
−
:* Manter seleccionado o libro na combo.
+
'''Arquivo: bd.php'''
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_LIBROS',
 +
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo'
 +
); </syntaxhighlight>
  
−
<u>Nota:</u> Ao principio ides ter problema de tipo 'non amosa ningún dato'. O primeiro que tedes que descartar é que a consulta SQL estea ben e devolva datos, polo que vos aconsello que fagades un 'echo' de dita consulta e a executedes no MysqlWorkBench para ver que realmente devolve algún resultado.
+
* Como norma para os nomes das constantes que conteñan sentenzas SQL:
  
 +
:* Comezarán cun verbo que poderá ser `CONSULTAR`, `INSERIR`, `MODIFICAR` ou `ELIMINAR`.
 +
:* A continuación empregarase un guión baixo.
 +
:* Despois indicarase o obxectivo da operación.
  
 +
==== Solución Caso Práctico 1 empregando PDO ====
  
−
* Crearemos unha nova páxina de nome '''consultar_exemplares_por_libro.php'''.
+
'''Arquivo: consultar_libros.php'''
  
−
: Modificamos o código para que aparezan os libros dentro da etiqueta <select> de html:
+
::<syntaxhighlight lang="php">
−
: Neste caso, na combo imos gardar o código do libro xa que cando escollamos un, necesitamos obter dito código para facer outra consulta buscando os exemplares dese libro...
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
 
−
ini_set('display_errors', 1);
+
ini_set('display_errors', '1');
−
ini_set('display_startup_errors', 1);
 
 
error_reporting(E_ALL);
 
error_reporting(E_ALL);
  
−
require ('/var/www/confBD/bd.inc.php');
+
require '/var/www/confBD/bd.php';
  
 
try {
 
try {
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
  
 +
    $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_LIBROS);
 +
 +
    $libros = $stmt->fetchAll();
 +
 +
} catch (PDOException $e) {
 +
 +
    error_log($e->getMessage());
 +
    die('Erro relacionado coa base de datos.');
 +
 +
} catch (Throwable $e) {
 +
 +
    die('Produciuse un erro na aplicación.');
 +
 +
}
  
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
<!DOCTYPE html>
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
 
−
    <head>
+
<html lang="gl">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
<head>
−
        <title>Operacións BD</title>
+
    <meta charset="UTF-8">
−
    </head>
+
    <title>Operacións BD</title>
 +
</head>
  
−
    <body>
+
<body>
−
    <?php       
 
−
       
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->num_rows>0){
 
−
            echo "<select name='lstLibros'>";
 
−
            echo "<option value='-1'></option>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                printf("<option value='%d'>%s</option>",htmlentities($row["id"]),htmlentities($row["titulo"]));
 
−
            }
 
−
            echo "</select>";
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
 
<?php
 
<?php
−
}  // Pecha o try da parte superior da páxina
+
 
−
catch (mysqli_sql_exception $e) {
+
if ($libros) {
−
     die("Erro mysql: " . $e->getMessage());
+
 
−
}
+
     echo "<table border='1'>";
−
catch (Expcetion $e) {
+
 
−
    die("Erro xeral: " . $e->getMessage());
+
    foreach ($libros as $libro) {
−
}
+
 
−
finally{
+
        echo '<tr>';
−
  if(!empty($conex)){
+
 
−
      $conex->close();
+
        printf(
−
  }
+
            '<td>%s</td>',
 +
            htmlspecialchars(
 +
                $libro['titulo'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            )
 +
        );
 +
 
 +
        echo '</tr>';
 +
    }
 +
 
 +
    echo '</table>';
 +
 
 +
} else {
 +
 
 +
    echo 'Non hai datos que amosar.';
 +
 
 
}
 
}
 +
 
?>
 
?>
 +
 +
</body>
 +
</html>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
:* Liña 32: Engadimos unha entrada en branco un valor -1 no seu value, para 'obrigar' a escoller un valor e que por defecto non apareza cun libro seleccionado.
+
* Neste caso empregamos `query()` porque a sentenza SQL non recibe ningún dato externo.
−
: Dependerá da aplicación, podedes ter que implementar variantes como ter unha entrada que indique 'TODAS' ou facer que por defecto estea seleccionada algunha das entradas...
+
 
 +
* `fetchAll()` recupera todas as filas do resultado.
 +
 
 +
: Esta solución é adecuada cando sabemos que o número de filas que imos recuperar é pequeno.
 +
 
 +
* Se o resultado puidese ter moitas filas, sería preferible procesalas unha a unha:
 +
 
 +
::<syntaxhighlight lang="php">
 +
while ($libro = $stmt->fetch()) {
 +
 
 +
echo htmlspecialchars(
 +
    $libro['titulo'],
 +
    ENT_QUOTES | ENT_SUBSTITUTE,
 +
    'UTF-8'
 +
);
 +
 
 +
} </syntaxhighlight>
 +
 
 +
* '''IMPORTANTE:''' Non empregaremos `rowCount()` para comprobar cantas filas devolveu unha sentenza `SELECT`.
 +
 
 +
: Se realmente necesitamos coñecer o número de rexistros, empregaremos `COUNT(*)`.
 +
 
 +
<br />
 +
 
 +
=== Caso Práctico 2 ===
  
−
: Resultado:
+
* Imos recuperar todos os autores ordenados polos seus apelidos e nome.
−
[[Imagen:Php_BD_9.jpg|300px|center]]
 
  
 +
* Cada autor aparecerá como un enlace co seguinte formato:
  
 +
::<syntaxhighlight lang="html"> <a href="detalle_autor.php?id=5">
 +
Apelidos, Nome </a> </syntaxhighlight>
  
 +
* O identificador será enviado mediante a URL.
  
−
* '''Arquivo bd.inc.php'''
+
: Que un identificador apareza na URL '''non supón por si mesmo un problema de seguridade'''.
−
: Definimos nunha constate a consulta que vai obter os exemplares do libro seleccionado:
+
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
: O importante é validar sempre o valor recibido na páxina de destino e comprobar que o usuario ten permiso para acceder ao recurso correspondente.
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id from exemplares where libro_id=%d order by estado_id');
+
 
−
</syntaxhighlight>
+
'''Arquivo: bd.php'''
−
:: Como vemos imos enviar o código por medio dun parámetro de tipo numérico (usaremos a función sprintf).
 
−
: Definimos nunha constante onde se atopan as imaxes relacionados cos libros:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define ('CARTAFOL_IMAXES_WEB','http://www.meusitio.es/IMAXES/');  // Debedes cambiar www.meusitioweb.es pola vosa URL ou se non tedes configurado un DNS ou arquivo hosts poñede http::/localhost/IMAXES/
 
−
</syntaxhighlight>
 
−
:: Teredes que cambiar a url pola vosa.
 
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_AUTORES',
 +
'SELECT id, apelidos, nome
 +
FROM autores
 +
ORDER BY apelidos, nome'
 +
); </syntaxhighlight>
  
 +
==== Solución Caso Práctico 2 empregando PDO ====
  
−
* Para facer que cando se seleccione un elemento do lista se 'active' o formulario, existen múltiples formas, unha delas pode ser:
+
'''Arquivo: consultar_autores.php'''
−
:::            echo "<select name='lstLibros' <b>onchange='this.form.submit()'</b>>";
 
−
::: Será necesario 'envolver' a etiqueta <select> cun <form>, con method='post' e action apuntando á mesma páxina.
 
  
−
:* O código que vai amosar os exemplares do libro seleccionado será o seguinte:
+
::<syntaxhighlight lang="php">
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="42,44,47-56" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
 
−
ini_set('display_errors', 1);
+
ini_set('display_errors', '1');
−
ini_set('display_startup_errors', 1);
 
 
error_reporting(E_ALL);
 
error_reporting(E_ALL);
  
−
require ('/var/www/confBD/bd.inc.php');
+
require '/var/www/confBD/bd.php';
  
 
try {
 
try {
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);   // Indico o nome da bd
+
 
−
     $conex->set_charset('utf8mb4');
+
    $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_AUTORES);
 +
 
 +
    $autores = $stmt->fetchAll();
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    die('Erro relacionado coa base de datos.');
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    die('Produciuse un erro na aplicación.');
 +
 
 +
}
 +
 
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
<!DOCTYPE html>
−
<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>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
<html lang="gl">
−
    <?php       
+
<head>
−
       
+
    <meta charset="UTF-8">
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
     <title>Autores</title>
−
        if ($result && $result->num_rows>0){
+
</head>
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
 
−
            echo "<select name='lstLibros' onChange='this.form.submit();'>";
 
−
            echo "<option value='-1'></option>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                printf("<option value='%d'>%s</option>",htmlentities($row["id"]),htmlentities($row["titulo"]));
 
−
            }
 
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
    ?>      
 
−
     <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
 
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
 
−
            echo "<div style='margin-top:40px'>";
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$conex->real_escape_string($_POST['lstLibros']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<table border='1'>";
 
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%.2f</td><td>%d</td>",
 
−
                            htmlentities($row["id_exemplar"]),htmlentities($row["imaxe"]),
 
−
                            CARTAFOL_IMAXES_WEB . htmlentities($row["imaxe"]),htmlentities($row["prezo"]),htmlentities($row["estado_id"]));
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
        // $conex->close();  // Non fai falla pechar a conexión xa que esta se pecha ao final do páxina web. Noutras páxinas pode ser necesario
 
−
       
 
−
    ?>      
 
  
−
    </body>   
+
<body>
−
</html>
 
  
 
<?php
 
<?php
−
}  // Pecha o try da parte superior da páxina
+
 
−
catch (mysqli_sql_exception $e) {
+
if ($autores) {
−
     die("Erro mysql: " . $e->getMessage());
+
 
 +
    echo '<ul>';
 +
 
 +
     foreach ($autores as $autor) {
 +
 
 +
        printf(
 +
            "<li><a href='detalle_autor.php?id=%d'>%s, %s</a></li>",
 +
            (int) $autor['id'],
 +
            htmlspecialchars(
 +
                $autor['apelidos'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            ),
 +
            htmlspecialchars(
 +
                $autor['nome'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            )
 +
        );
 +
    }
 +
 
 +
    echo '</ul>';
 +
 
 +
} else {
 +
 
 +
    echo 'Non hai datos que amosar.';
 +
 
 
}
 
}
−
catch (Expcetion $e) {
+
 
−
    die("Erro xeral: " . $e->getMessage());
+
?>
−
}
+
 
−
finally{
+
</body>
−
  if(!empty($conex)){
+
</html>
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
::* Liña 42: Comprobamos que veñan datos no formulario (que se escollera algún exemplar da lista) e que sexa de tipo numérico. Tamén comprobamos se seleccionamos o elemento baleiro (valor -1 na combo).
+
* Para amosar texto dentro de HTML empregamos `htmlspecialchars()`.
−
::* Liña 44: Definimos a consulta a realizar contra a base de datos, utilizando a constante definida e substituíndo o parámetro polo código do libro seleccionado.
+
 
−
::* Liñas 47-52: Construímos a táboa para que amose os datos. Fixarse que na imaxe temos que definir o atributo 'alt' para que cumpra o estándar XHTML, polo que enviamos dúas veces o nome do arquivo. No dato que vai no atributo 'src' enviamos o nome do arquivo xunto coa URL onde se atopan as imaxes.
+
* Non debemos empregar `urlencode()` sobre o nome e os apelidos, xa que estes valores non forman parte da URL senón do texto visible da páxina.
 +
 
 +
<br />
 +
 
 +
=== Consulta enviando parámetros de tipo numérico ===
 +
 
 +
* Cando unha consulta necesita recibir datos externos empregaremos unha '''sentenza preparada''' mediante:
 +
 
 +
::<syntaxhighlight lang="php">
 +
prepare()
 +
execute() </syntaxhighlight>
 +
 
 +
* Por exemplo:
 +
 
 +
::<syntaxhighlight lang="sql">
 +
SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor </syntaxhighlight>
 +
 
 +
* Definimos a consulta:
 +
 
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTA_CON_PARAM_NUMERICO',
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor'
 +
); </syntaxhighlight>
 +
 
 +
* Podemos executala desta forma:
  
−
::* Liña 62: Non pechamos a conexión xa que esta se pecha ao final de todo. Sempre que se abra unha conexión temos que ter o código que a pecha. Pode suceder que na mesma páxina abramos e pechemos varias veces a conexión ao introducir código con include.
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTA_CON_PARAM_NUMERICO
 +
);
  
 +
$stmt->execute([
 +
'valor' => 5
 +
]); </syntaxhighlight>
  
 +
* O nome do parámetro debe coincidir co indicado na sentenza SQL.
  
 +
: Se a consulta emprega:
  
−
<br />
+
::<syntaxhighlight lang="sql">
−
* Intenta agora modificar o código anterior e fai que o select manteña seleccionado o libro cando aparecen os exemplares.
+
:valor </syntaxhighlight>
  
 +
: debemos empregar:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt->execute([
 +
'valor' => 5
 +
]); </syntaxhighlight>
  
 +
==== Indicar explicitamente o tipo de dato ====
  
 +
* Cando enviamos os valores directamente mediante o array de `execute()`, PDO trátaos por defecto como `PDO::PARAM_STR`.
  
−
<br />
+
: Na maioría das consultas MySQL realizará as conversións necesarias segundo o contexto.
−
: Posible solución:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="7-10" >
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->num_rows>0){
 
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
 
−
            echo "<select name='lstLibros' onChange='this.form.submit();'>";
 
−
            echo "<option value='-1'></option>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                $cadea = '';
 
−
                if(isset($_POST['lstLibros']) && $row['id']==$_POST['lstLibros']){  // Se o libro seleccionado é igual ao id da fila, o seleccionamos en html
 
−
                    $cadea="selected='selected'";
 
−
                }
 
  
−
                printf("<option value='%d' %s>%s</option>",htmlentities($row["id"]),$cadea,htmlentities($row["titulo"]));
+
* Se queremos indicar explicitamente o tipo empregaremos `bindValue()`:
−
            }
 
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
  
−
</syntaxhighlight>
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTA_CON_PARAM_NUMERICO
 +
);
  
 +
$stmt->bindValue(
 +
':valor',
 +
5,
 +
PDO::PARAM_INT
 +
);
  
 +
$stmt->execute(); </syntaxhighlight>
  
 +
* Algúns dos tipos dispoñibles son:
  
 +
::<syntaxhighlight lang="text">
 +
PDO::PARAM_INT
 +
PDO::PARAM_STR
 +
PDO::PARAM_BOOL
 +
PDO::PARAM_NULL </syntaxhighlight>
  
 +
* É especialmente recomendable indicar explicitamente o tipo cando este teña importancia para a consulta, por exemplo con `LIMIT` ou `OFFSET`.
  
 
<br />
 
<br />
−
* O código completo sería o seguinte:
 
  
−
'''Arquivo: bd.inc.php''':
+
==== Caso práctico: Enviando parámetros de tipo numérico ====
 +
 
 +
* Imos facer que o usuario poida seleccionar un libro nunha lista `<select>` e que se amosen os exemplares correspondentes.
 +
: Terás que programar que o libro se manteña seleccionado cando cambiemos de libro na lista. Para iso recorda que tes o libro selecccionado e cando creas a lista en html terás que comprobar si e o libro seleccionado, dentro do bucle que xera as etiquetas <option>, coincide coa etiqueta option seleccionada e se é o caso, poñela 'selected'.
 +
 
 +
[[Imagen:Php_BD_9.jpg|800px|center]]
 +
 
 +
 
 +
 
 +
 
 +
* Primeiro definimos as consultas.
 +
 
 +
'''Arquivo: bd.php'''
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
::<syntaxhighlight lang="php">
−
<?php
+
define(
 +
'CONSULTAR_LIBROS',
 +
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo'
 +
);
  
−
define ('SERVIDOR','localhost');
+
define(
−
define ('USUARIO','user_php');
+
'CONSULTAR_EXEMPLARES_POR_LIBRO',
−
define ('PASSWORD','user_php');
+
'SELECT libro_id,
−
define ('BD','php');
+
id_exemplar,
 +
imaxe,
 +
prezo,
 +
estado_id
 +
FROM exemplares
 +
WHERE libro_id = :libro_id
 +
ORDER BY estado_id'
 +
);
  
 +
define(
 +
'CARTAFOL_IMAXES_WEB',
 +
'http://localhost/IMAXES/'
 +
); </syntaxhighlight>
  
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
+
===== Solución empregando PDO =====
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
+
: Fixarse que
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id'
 
−
            . ' from exemplares '  // Necesita un espazo en branco antes do from para concatenar có anterior
 
−
            . ' where libro_id=%d order by titulo');  //  Necesita un espazo en branco antes do where para concatenar có anterior
 
−
define ('CARTAFOL_IMAXES_WEB','http://localhost/IMAXES/');
 
  
−
</syntaxhighlight>
 
  
 +
'''Arquivo: consultar_exemplares_por_libro.php'''
  
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
::<syntaxhighlight lang="php">
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
 
−
ini_set('display_errors', 1);
+
ini_set('display_errors', '1');
−
ini_set('display_startup_errors', 1);
 
 
error_reporting(E_ALL);
 
error_reporting(E_ALL);
  
−
require ('/var/www/confBD/bd.inc.php');
+
require '/var/www/confBD/bd.php';
 +
 
 +
$libros = [];
 +
$exemplares = [];
 +
 
 +
$libroSeleccionado = filter_input(
 +
    INPUT_POST,
 +
    'lstLibros',
 +
    FILTER_VALIDATE_INT
 +
);
  
 
try {
 
try {
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);   // Indico o nome da bd
+
 
−
     $conex->set_charset('utf8mb4');
+
    $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
 +
        ]
 +
    );
 +
 
 +
    /*
 +
    * Obtemos os libros que se van amosar
 +
    * no elemento select.
 +
    */
 +
    $stmt = $conex->query(CONSULTAR_LIBROS);
 +
 
 +
    $libros = $stmt->fetchAll();
 +
 
 +
 
 +
    /*
 +
    * Se o usuario seleccionou un libro válido,
 +
    * consultamos os seus exemplares.
 +
    */
 +
    if (
 +
        $libroSeleccionado !== false &&
 +
        $libroSeleccionado !== null &&
 +
        $libroSeleccionado !== -1
 +
     ) {
 +
 
 +
        $stmt = $conex->prepare(
 +
            CONSULTAR_EXEMPLARES_POR_LIBRO
 +
        );
 +
 
 +
        $stmt->bindValue(
 +
            ':libro_id',
 +
            $libroSeleccionado,
 +
            PDO::PARAM_INT
 +
        );
 +
 
 +
        $stmt->execute();
 +
 
 +
        $exemplares = $stmt->fetchAll();
 +
    }
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    die('Erro relacionado coa base de datos.');
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    die('Produciuse un erro na aplicación.');
 +
 
 +
}
  
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
<!DOCTYPE html>
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
 
−
    <head>
+
<html lang="gl">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
<head>
−
        <title>Operacións BD</title>
+
    <meta charset="UTF-8">
−
     </head>
+
    <title>Exemplares por libro</title>
 +
</head>
 +
 
 +
<body>
 +
 
 +
<form method="post">
 +
 
 +
<select
 +
    name="lstLibros"
 +
    onchange="this.form.submit()"
 +
>
 +
 
 +
    <option value="-1">
 +
        -- Escolle un libro --
 +
     </option>
  
−
    <body>
+
     <?php foreach ($libros as $libro): ?>
−
     <?php      
 
−
       
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->num_rows>0){
 
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
 
−
            echo "<select name='lstLibros' onChange='this.form.submit();'>";
 
−
            echo "<option value='-1'></option>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                $cadea = '';
 
−
                if(isset($_POST['lstLibros']) && $row['id']==$_POST['lstLibros']){  // Se o libro seleccionado é igual ao id da fila, o seleccionamos en html
 
−
                    $cadea="selected='selected'";
 
−
                }
 
  
−
                printf("<option value='%d' %s>%s</option>",htmlentities($row["id"]),$cadea,htmlentities($row["titulo"]));
+
        <option
 +
            value="<?= (int) $libro['id'] ?>"
 +
            <?php
 +
            if (
 +
                $libroSeleccionado !== false &&
 +
                $libroSeleccionado !== null &&
 +
                (int) $libro['id'] === $libroSeleccionado
 +
            ) {
 +
                echo 'selected';
 
             }
 
             }
−
             echo "</select>";
+
             ?>
−
            printf("</form>");
+
        >
−
             $result->free();  // Liberamos da memoria os recursos
+
             <?= htmlspecialchars(
−
        }
+
                $libro['titulo'],
−
        else {
+
                ENT_QUOTES | ENT_SUBSTITUTE,
−
            echo "Non hai datos que amosar!!!!";
+
                 'UTF-8'
−
        }
+
            ) ?>
−
    ?>       
+
        </option>
−
    <?php       
+
 
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
+
    <?php endforeach; ?>
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
+
 
−
            echo "<div style='margin-top:40px'>";
+
</select>
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$conex->real_escape_string($_POST['lstLibros']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                 echo "<table border='1'>";
 
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%.2f</td><td>%d</td>",
 
−
                            htmlentities($row["id_exemplar"]),htmlentities($row["imaxe"]),
 
−
                            CARTAFOL_IMAXES_WEB . htmlentities($row["imaxe"]),htmlentities($row["prezo"]),htmlentities($row["estado_id"]));
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
        // $conex->close();  // Non fai falla pechar a conexión xa que esta se pecha ao final do páxina web. Noutras páxinas pode ser necesario
 
−
       
 
−
    ?>      
 
  
−
    </body>   
+
</form>
−
</html>
 
  
−
<?php
+
<?php if (
−
}  // Pecha o try da parte superior da páxina
+
     $libroSeleccionado !== false &&
−
catch (mysqli_sql_exception $e) {
+
    $libroSeleccionado !== null &&
−
     die("Erro mysql: " . $e->getMessage());
+
     $libroSeleccionado !== -1
−
}
+
): ?>
−
catch (Expcetion $e) {
 
−
     die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
<?php if ($exemplares): ?>
  
 +
    <table border="1">
  
 +
        <tr>
 +
            <th>Num. exemplar</th>
 +
            <th>Imaxe</th>
 +
            <th>Prezo</th>
 +
            <th>Estado</th>
 +
        </tr>
  
−
<br />
+
        <?php foreach ($exemplares as $exemplar): ?>
−
* '''<u>Exercicio proposto:</u>''' Cando aparecen os exemplares do libro seleccionado, fai que apareza enriba da táboa, dentro dun &lt;h3&mt; o nome do libro seleccionado.
 
  
 +
            <tr>
  
 +
                <td>
 +
                    <?= (int) $exemplar['id_exemplar'] ?>
 +
                </td>
  
 +
                <td>
 +
                    <img
 +
                        src="<?= htmlspecialchars(
 +
                            CARTAFOL_IMAXES_WEB .
 +
                            ($exemplar['imaxe'] ?? ''),
 +
                            ENT_QUOTES | ENT_SUBSTITUTE,
 +
                            'UTF-8'
 +
                        ) ?>"
 +
                        alt="<?= htmlspecialchars(
 +
                            $exemplar['imaxe'] ?? '',
 +
                            ENT_QUOTES | ENT_SUBSTITUTE,
 +
                            'UTF-8'
 +
                        ) ?>"
 +
                        width="42"
 +
                        height="42"
 +
                    >
 +
                </td>
  
 +
                <td>
 +
                    <?= number_format(
 +
                        (float) $exemplar['prezo'],
 +
                        2,
 +
                        ',',
 +
                        '.'
 +
                    ) ?>
 +
                </td>
  
 +
                <td>
 +
                    <?= (int) $exemplar['estado_id'] ?>
 +
                </td>
  
−
<br />
+
            </tr>
−
* Imos realizar <u>unha mellora</u> nos datos que se visualizan.
 
−
: Como vemos, aparece o código do estado do libro. O que queremos amosar é a descrición, polo que teremos que facer un Join coa táboa ESTADOS.
 
  
−
'''Arquivo bd.inc.php''':
+
        <?php endforeach; ?>
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id,estados.descripcion as desc_estado'
 
−
            . ' from exemplares INNER JOIN estados ON (estados.id=exemplares.estado_id)'  // Atención aos espazos en branco no from. Se non os pos quedaría mal a sentenza. Lembra facer un echo da sentenza para ver que todo estea ben
 
−
            . ' where libro_id=%d order by desc_estado'); //  Necesita un espazo en branco antes do where para concatenar có anterior
 
−
</syntaxhighlight>
 
  
 +
    </table>
  
 +
<?php else: ?>
  
−
<br />
+
    <p>Non hai exemplares que amosar.</p>
−
'''Arquivo: consultar_exemplares_por_libro.php''':
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="14" >
 
−
        ..................
 
  
−
    <?php      
+
<?php endif; ?>
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
 
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
 
−
            echo "<div style='margin-bottom:40px'>";
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$conex->real_escape_string($_POST['lstLibros']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<table border='1'>";
 
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%.2f</td><td>%s</td>",
 
−
                            htmlentities($row["id_exemplar"]),htmlentities($row["imaxe"]),CARTAFOL_IMAXES_WEB . htmlentities($row["imaxe"]),
 
−
                            htmlentities($row["prezo"]),htmlentities($row["desc_estado"]));
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
       
 
−
    ?>  
 
  
−
        ..................
+
<?php endif; ?>
  
 +
</body>
 +
</html>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* Liña 14: Modificamos o tipo de parámetro que pasa de ser numérico (%d) a cadea (%s) e amosamos a descrición do estado (as dúas columnas do select non deben ter o mesmo nome, descrición do libro e descrición do estado).
+
* Neste exemplo empregamos `filter_input()` con `FILTER_VALIDATE_INT` para comprobar que o dato recibido é un número enteiro válido.
  
 +
* Despois utilizamos unha sentenza preparada e:
  
 +
::<syntaxhighlight lang="php">
 +
PDO::PARAM_INT </syntaxhighlight>
  
−
* Resultado:
+
: para indicar explicitamente que o identificador é numérico.
  
−
[[Imagen:Php_BD_12.jpg|400px|center]]
+
* Non necesitamos empregar `mysqli_real_escape_string()` nin concatenar o dato dentro da consulta.
  
 +
==== Amosar a descrición do estado ====
  
 +
* Podemos mellorar a consulta realizando un `JOIN` coa táboa `estados` para amosar o estado do exemplar.
  
 +
[[Imagen:Php_BD_12.jpg|800px|center]]
  
  
 +
'''Arquivo: bd.php'''
 +
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_EXEMPLARES_POR_LIBRO',
 +
'SELECT exemplares.libro_id,
 +
exemplares.id_exemplar,
 +
exemplares.imaxe,
 +
exemplares.prezo,
 +
estados.descripcion AS desc_estado
 +
FROM exemplares
 +
INNER JOIN estados
 +
ON estados.id = exemplares.estado_id
 +
WHERE exemplares.libro_id = :libro_id
 +
ORDER BY desc_estado'
 +
); </syntaxhighlight>
 +
 +
* Para amosar o estado:
 +
 +
::<syntaxhighlight lang="php">
 +
echo htmlspecialchars(
 +
$exemplar['desc_estado'],
 +
ENT_QUOTES | ENT_SUBSTITUTE,
 +
'UTF-8'
 +
); </syntaxhighlight>
  
 
<br />
 
<br />
−
'''EXERCICIO PROPOSTO''':
 
  
−
: Crea unha páxina de nome '''consultar_exemplares_por_estado.php''' no que aparezan en forma de radiobutton os estados da táboa ESTADOS.
+
==== Exercicio proposto ====
−
: Ao seleccionar un estado, amosaranse en forma de lista ordenada os exemplares (libro_id,num_exemplar,prezo)  ordenados por id (quen queira pode facer un join coa táboa LIBROS e amosar o título en vez do id e ordenar os resultados polo título).
 
−
: O radiobutton debe manterse seleccionado.
 
  
 +
* Crea unha páxina chamada `consultar_exemplares_por_estado.php`.
  
 +
* Debe:
  
 +
:* Amosar os estados da táboa `ESTADOS` mediante botóns de opción (`radio`).
 +
:* Ao seleccionar un estado, realizarase automaticamente o envío do formulario.
 +
:* O estado seleccionado debe manterse marcado.
 +
:* Amosaranse os exemplares correspondentes ao estado seleccionado.
 +
:* A consulta deberá realizarse mediante PDO e unha sentenza preparada.
 +
:* O identificador do estado deberá validarse como número enteiro.
 +
:* Empregarase `PDO::PARAM_INT` para enviar o parámetro.
  
  
  
 +
[[Imagen:Php_BD_13.jpg|800px|center]]
  
−
<br />
+
:<u>Nota:</u> Podedes darlle un formato máis 'bonito' con CSS.
−
'''POSIBLE SOLUCIÓN''':
 
  
−
'''Arquivo bd.inc.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('CONSULTAR_ESTADOS','select id,descripcion from estados order by descripcion');
 
−
define ('CONSULTAR_EXEMPLARES_POR_ESTADO','select titulo,id_exemplar,prezo '
 
−
        . ' from exemplares INNER JOIN libros ON (libros.id = exemplares.libro_id)'
 
−
        .                ' INNER JOIN estados ON (estados.id = exemplares.estado_id)'
 
−
        . ' where estado_id=%d order by titulo');
 
  
−
</syntaxhighlight>
+
* Consultas propostas:
  
−
: Definimos dúas consultas. Unha para buscar os estados e outra para buscar os exemplares que teñan un estado determinado (pasado como parámetros)
+
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_ESTADOS',
 +
'SELECT id, descripcion
 +
FROM estados
 +
ORDER BY descripcion'
 +
);
  
 +
define(
 +
'CONSULTAR_EXEMPLARES_POR_ESTADO',
 +
'SELECT libros.titulo,
 +
exemplares.id_exemplar,
 +
exemplares.prezo
 +
FROM exemplares
 +
INNER JOIN libros
 +
ON libros.id = exemplares.libro_id
 +
WHERE exemplares.estado_id = :estado_id
 +
ORDER BY libros.titulo'
 +
); </syntaxhighlight>
  
−
'''Arquivo consultar_exemplares_por_estado.php'''
+
<br />
−
::<syntaxhighlight lang="java" line 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.inc.php');
+
=== Caso práctico: Enviando parámetros de tipo cadea ===
  
−
try {
+
* Imos crear unha páxina que permita '''buscar libros polo nome dun autor'''.
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
+
: O que se pretende programar é algo parecido a isto:
−
    $conex->set_charset('utf8mb4');
+
[[Imagen:Php_BD_11.jpg|800px|center]]
−
?>
 
  
−
<!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>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
 
−
    <body>
 
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
 
  
−
        <?php       
+
: O usuario introducirá nunha caixa de texto o nome, os apelidos ou unha parte deles.
−
        $cont=0;    // Conta o número de estados impresos e cando leve múltiplo de dous salta de liña
 
−
        $result=$conex->query(CONSULTAR_ESTADOS);
 
−
        if ($result && $result->num_rows>0){
 
−
            while($row = $result->fetch_assoc()) {
 
−
                $cont++;
 
−
                if(isset($_POST['rbgrpEstado']) && $row['id']==$_POST['rbgrpEstado']){  // Se volve a cargar a páxina e ven cun estado, o seleccionamos
 
−
                    $cadea="checked='checked'";
 
−
                }
 
−
                else{
 
−
                    $cadea='';
 
−
                }
 
−
                printf("<input onclick='javascript:this.form.submit()' type='radio' name='rbgrpEstado' value='%d' %s>%s</input>",
 
−
                        $row["id"],$cadea,$row["descripcion"]);
 
−
                if(($cont%2)==0){  // Cada par cambiamos de liña
 
−
                    echo "<br />";
 
−
                }
 
−
            }
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai estados que amosar!!!!";
 
−
        }
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
       
 
−
        <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN ESTADO SELECCIONADO
 
−
        if (!empty($_POST['rbgrpEstado']) && filter_var($_POST['rbgrpEstado'],FILTER_VALIDATE_INT)){
 
−
           
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_ESTADO,$conex->real_escape_string($_POST['rbgrpEstado']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<ol>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<li>';
 
−
                    printf("T&iacute;tulo:%s - Num_exemplar:%d - Prezo:%s",$row["titulo"],$row["id_exemplar"],$row["prezo"]);
 
−
                    echo '</li>';
 
−
                }
 
−
                echo '</ol>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
           
 
−
        }
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
  </body>   
 
−
</html>
 
  
 +
: Por exemplo, se introduce:
  
−
<?php
+
::<syntaxhighlight lang="text">
−
}  // Pecha o try da parte superior da páxina
+
gar </syntaxhighlight>
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>  
 
−
</syntaxhighlight>  
 
  
 +
: poderían atoparse autores como `García`, `Vargas`, etc., dependendo dos datos existentes na base de datos.
  
 +
* Para realizar a busca empregaremos:
  
 +
:* Un formulario HTML con método `POST`.
 +
:* Unha consulta SQL co operador `LIKE`.
 +
:* Unha '''sentenza preparada PDO''' para enviar o texto introducido polo usuario.
 +
:* O parámetro `PDO::PARAM_STR`, xa que o dato enviado é unha cadea.
 +
:* `fetchAll()` para recuperar os resultados.
 +
:* `htmlspecialchars()` cando amosemos datos dentro do HTML.
  
−
* Resultado:
+
* A consulta devolverá:
  
−
[[Imagen:Php_BD_13.jpg|400px|center]]
+
:* O nome completo do autor.
 +
:* O identificador do libro.
 +
:* O título do libro.
  
 +
* Como un mesmo autor pode ter varios libros, evitaremos repetir o seu nome en todas as filas da táboa.
  
 +
* Ademais, o título de cada libro será un enlace que permitirá abrir unha nova xanela coa páxina:
  
 +
::<syntaxhighlight lang="text">
 +
detalle_libro.php </syntaxhighlight>
  
−
<br />
+
: á que enviaremos o identificador do libro mediante `GET`.
−
=== Consulta enviando parámetros de tipo cadea ===
 
  
−
* Cando utilicemos datos de tipo cadea, debemos lembrar poñelos entre comillas simples ('dato') na orde SQL.
+
: Para abrir esa páxina nunha nova xanela empregaremos JavaScript e a función `window.open()`.
  
 +
: Polo tanto, o funcionamento xeral será:
  
−
<u>Nota:</u> Lembra que no paso de datos temos que ter en conta os posibles 'ataques' que poidan intentar facerse. Repasar o dado [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios na wiki anteriormente].
+
::<syntaxhighlight lang="text">
 +
Usuario introduce o nome do autor
 +
↓
 +
Validamos o texto
 +
↓
 +
prepare() + bindValue()
 +
↓
 +
Consulta con LIKE
 +
↓
 +
Recuperamos os libros
 +
↓
 +
Amosamos autor e título
 +
↓
 +
Preme sobre un título
 +
↓
 +
window.open()
 +
↓
 +
detalle_libro.php?libro=ID </syntaxhighlight>
  
 +
===== Arquivo bd.php =====
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
+
* Engadimos a consulta que permite buscar libros polo nome ou apelidos dun autor:
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
 
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col2=''''%d''''
+
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_LIBROS_POR_AUTOR',
 +
"SELECT
 +
CONCAT(
 +
autores.apelidos,
 +
', ',
 +
autores.nome
 +
) AS nome_autor,
 +
libros.id AS id_libro,
 +
libros.titulo
 +
FROM libros
 +
INNER JOIN libro_autor
 +
ON libro_autor.libro_id = libros.id
 +
INNER JOIN autores
 +
ON libro_autor.autor_id = autores.id
 +
WHERE CONCAT(
 +
autores.nome,
 +
' ',
 +
autores.apelidos
 +
) LIKE :autor
 +
ORDER BY nome_autor, libros.titulo"
 +
); </syntaxhighlight>
  
−
: No exemplo, col2 é de tipo cadea (por exemplo, varchar ou char).
+
* A consulta une as táboas:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('CONSULTA_CON_PARAM_CADEA',"SELECT col1, col2 FROM TABOA_1 WHERE col2='%s'");
 
  
−
$query = sprintf(CONSULTA_CON_PARAM_CADEA,'valor_cadea_a_enviar');
+
::<syntaxhighlight lang="text">
−
</syntaxhighlight>  
+
libros
−
: Fixarse como o parámetro col2 vai entrecomillado e o formato do parámetro é '%s'.
+
libro_autor
 +
autores </syntaxhighlight>
  
 +
: para obter os libros correspondentes a cada autor.
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
* Mediante:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
......
 
−
$result=$conex->query(CONSULTA_CON_PARAM_CADEA);
 
−
......
 
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="sql">
 +
CONCAT(
 +
autores.nome,
 +
' ',
 +
autores.apelidos
 +
) </syntaxhighlight>
  
 +
: construímos unha cadea formada polo nome e os apelidos do autor.
  
 +
* Sobre esa cadea aplicamos:
  
−
<br />
+
::<syntaxhighlight lang="sql">
−
==== Caso práctico: Enviando parámetros de tipo cadea ====
+
LIKE :autor </syntaxhighlight>
  
−
* Imos a aplicalo ao noso exemplo e imos crear unha páxina no que buscaremos libros en base a un nome do autor.
+
: sendo `:autor` o parámetro que enviaremos posteriormente desde PDO.
−
:: A busca farase utilizando a [http://www.w3schools.com/sql/sql_like.asp orde SQL '''like'''], que busca patróns de cadea. Este operador non distingue maiúsculas de minúsculas.
 
−
:: Se queremos distinguir maiúsculas de minúsculas utilizaremos o operador [http://stackoverflow.com/questions/14007450/how-do-you-force-mysql-like-to-be-case-sensitive like binary].
 
−
:: O operador like o imos aplicar ao nome completo do autor, polo que teremos que concatenar o nome e apelido. Usaremos a función [http://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_concat-ws concat_ws] ou [http://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_concat concat].
 
  
 +
* O parámetro debe representar o valor completo.
  
−
* O usuario introducirá nunha caixa de texto o nome (ou parte del) do autor.
+
: Polo tanto, non escribiremos:
  
−
: <u>Nota:</u> Lembra que no paso de datos temos que ter en conta os posibles 'ataques' que poidan intentar facerse. Repasar o dado [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios na wiki anteriormente].
+
::<syntaxhighlight lang="sql">
 +
LIKE '%:autor%' </syntaxhighlight>
  
−
:: Mysql incorpora tamén unha función para codificar os caracteres (como espazos en branco, saltos de liña,...[http://php.net/manual/es/mysqli.real-escape-string.php Función string mysqli::real_escape_string ( string $escapestr )]. Devolve a cadea 'codificada'.
+
: senón que os caracteres `%` serán engadidos ao valor desde PHP:
−
:: Por exemplo:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$cidade = "'s Hertogenbosch";
 
  
−
/* esta consulta fallará debido a que no escapa $ciudad */
+
::<syntaxhighlight lang="php">
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
+
'%' . $textoBuscar . '%' </syntaxhighlight>
−
    printf("Erro: %s\n", $mysqli->sqlstate);
 
−
}
 
  
−
$cidade = $mysqli->real_escape_string($cidade);
+
===== Solución completa =====
  
−
/* esta consulta con $ciudad escapada funcionará */
+
'''Arquivo: buscar_libros_por_autor.php'''
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
 
−
    printf("Fila inxerida.\n");
 
−
}
 
  
−
</syntaxhighlight>
+
::<syntaxhighlight lang="php">
−
::Exemplo obtido de php.net
+
<?php
  
 +
/*
 +
* Estas directivas utilízanse durante o desenvolvemento.
 +
* Nun servidor en produción non se deberían amosar
 +
* os erros PHP ao usuario.
 +
*/
 +
ini_set('display_errors', '1');
 +
error_reporting(E_ALL);
  
−
* Pode suceder que un mesmo autor teña varios libros. Como imos a amosar os datos dentro dun grid no que na primeira columna vai ser o nome do autor e a segunda o nome do libro, vai repetirse o nome do autor como se amosa na seguinte imaxe:
+
require '/var/www/confBD/bd.php';
  
−
[[Imagen:Php_BD_10.jpg|500px|center]]
 
  
−
:: Para evitalo, teremos que gardar en cada iteración unha vez 'impresa' a liña do autor, o nome do autor e comparalo cando carguemos a seguinte fila de datos. Se o nome do autor da nova liña de datos é igual ao anterior, entón non o debemos de amosar.
+
/*
 +
* Variables que utilizaremos posteriormente no HTML.
 +
*/
 +
$erro = '';
 +
$libros = [];
  
−
:: O código sería algo parecido a isto:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
                    $autor='';  // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                    $last_autor=''; // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                    while($row = $result->fetch_assoc()) {    // PROCESAMOS AS FILAS DO RESULTADO DA CONSULTA
 
−
                        if ($last_autor==$row["nome_autor"]){  // Comprobamos se o novo autor é igual ao anterior procesado
 
−
                            $autor='';          // Se é igual entón quere dicir que estamos a ler unha fila co mesmo autor que a anterior. Polo tanto non 'imprimimos o autor'
 
−
                        }
 
−
                        else {
 
−
                            $autor=$row["nome_autor"];  // En caso contrario quere dicir que o autor actual é diferente ao anterior e polo tanto debemos sacalo por pantalla.
 
−
                        }
 
  
−
                        printf(AMOSAMOS OS DATOS DO LIBRO E DA VARIABLE $autor);
+
/*
 +
* Recuperamos o valor enviado polo formulario.
 +
*
 +
* O operador ?? permite indicar unha cadea baleira
 +
* se o formulario aínda non foi enviado.
 +
*
 +
* trim() elimina espazos en branco ao principio
 +
* e ao final da cadea.
 +
*/
 +
$textoBuscar = trim(
 +
    $_POST['txtAutor'] ?? ''
 +
);
  
−
                        $last_autor=$row["nome_autor"];  // Gardamos o nome do autor para comparar có autor que veña na seguinte fila de datos.
 
−
                    }
 
  
−
</syntaxhighlight>
+
try {
  
 +
    /*
 +
    * Creamos o DSN.
 +
    */
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
  
−
* Imos facer que cada libro sexa un enlace para abrir unha páxina nova de nome '''detalle_libro.php'''. No resultado da busca soamente imos amosar o nome do libro, de tal forma que cando o usuario preme sobre o enlace, apareza unha nova xanela cos datos completos dese libro, incluíndo os exemplares).
+
    /*
 +
    * Abrimos a conexión.
 +
    *
 +
    * Os datos SERVIDOR, USUARIO, PASSWORD e BD
 +
    * están definidos no arquivo bd.php.
 +
    */
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
:: Para facelo imos facer uso de javascript e da [http://www.w3schools.com/jsref/met_win_open.asp función window.open].
 
−
:: Crearei unha función en javascript que espere recibir un parámetro, que sexa a url a abrir e chamarei a window.open cunha serie de modificacións, como o tamaño da xanela, que non poida modificar a url,...
 
−
:: O código será o seguinte:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4-8" >
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
        <script type="text/javascript">
 
−
            function abrirVentana($url){
 
−
                window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
 
−
            }
 
−
        </script>
 
−
       
 
−
    </head>
 
−
</syntaxhighlight>
 
  
 +
    /*
 +
    * Comprobamos se o formulario foi enviado.
 +
    */
 +
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  
−
:: Para chamar a esta función usaremos a etiqueta <a> e o evento onclick, coma o seguinte exemplo:
+
        /*
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4-8" >
+
        * Comprobamos que se introduciu algún texto.
−
<a href='#' onclick="javascript:abrirVentana('detalle_libro.php?libro=1');">Título Libro</a>
+
        */
−
</syntaxhighlight>
+
        if ($textoBuscar === '') {
  
−
:: <u>Hai que ter moito coidado cando estamos a traballar con PHP-Javascript polo tema das comillas xa que teremos que 'xogar' coas simples e dobres para que se pechen correctamente</u>.
+
            $erro = 'Debes introducir un texto para realizar a busca.';
  
 +
            /*
 +
        * Como regra da aplicación esiximos
 +
        * polo menos 3 caracteres.
 +
        */
 +
        } elseif (mb_strlen($textoBuscar) < 3) {
  
 +
            $erro =
 +
                'A cadea a buscar debe ter polo menos 3 caracteres.';
 +
        } else {
  
 +
            /*
 +
            * Preparamos a consulta.
 +
            */
 +
            $stmt = $conex->prepare(
 +
                CONSULTAR_LIBROS_POR_AUTOR
 +
            );
  
  
−
<br />
+
            /*
−
* '''SOLUCIÓN'''
+
            * Engadimos os comodíns % de LIKE ao valor
 +
            * que enviamos ao parámetro.
 +
            *
 +
            * Por exemplo:
 +
            *
 +
            * gar
 +
            *
 +
            * convértese en:
 +
            *
 +
            * %gar%
 +
            */
 +
            $stmt->bindValue(
 +
                ':autor',
 +
                '%' . $textoBuscar . '%',
 +
                PDO::PARAM_STR
 +
            );
  
−
'''Arquivo: bd.inc.php'''
 
  
−
<u>Nota:</u> Definimos a consulta a realizar á base de datos ('''lembrar que é mellor facer dita consulta directamente no Mysql e despois copiala ao código PHP pasando os parámetros correspondentes''')
+
            /*
−
::<syntaxhighlight lang="java" enclose="div">
+
            * Executamos a consulta.
−
<?php
+
            */
 +
            $stmt->execute();
  
−
define ('CONSULTAR_LIBROS_POR_AUTOR',"select CONCAT(apelidos,',',nome) as nome_autor,libros.id ,titulo from libros " .
 
−
        ' INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id) ' .
 
−
        ' INNER JOIN autores ON (libro_autor.autor_id=autores.id)' .
 
−
        " where CONCAT(autores.nome,' ',autores.apelidos) like '%%%s%%' " .
 
−
        'order by nome_autor,titulo');
 
−
</syntaxhighlight>
 
  
−
: <u>Nota:</u> Fixarse como o parámetro que recibe 'like' é de tipo cadea e polo tanto vai entre comillas e leva dobre % para que escriba o % non o interprete como un parámetro do printf.
+
            /*
−
: É dicir, nos o que queremos é que execute a sentenza: like '%palabraabuscar%' pero esa 'palabra a buscar' vai ser %s (un parámetro cadea a empregar en printf). Entón non podemos poñer %%s% xa que printf interpretará eses % como parámetros a enviar. Temos que decirlle que o % inicial e final non debe interpretalos como parámetro e iso se fai poñenedo %%.
+
            * Recuperamos todos os resultados.
 +
            */
 +
            $libros = $stmt->fetchAll();
  
  
 +
            /*
 +
            * Se non hai filas gardamos unha mensaxe
 +
            * para amosala posteriormente.
 +
            */
 +
            if (!$libros) {
  
 +
                $erro =
 +
                    'Non se atoparon libros para ese autor.';
 +
            }
 +
        }
 +
    }
 +
} catch (PDOException $e) {
  
−
<br />
+
    /*
−
'''Arquivo buscar_libros_por_autor.php'''
+
    * Rexistramos a información técnica no log.
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
    * Non debemos amosala ao usuario.
−
<?php
+
    */
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
    error_log(
−
ini_set('display_errors', 1);
+
        $e->getMessage()
−
ini_set('display_startup_errors', 1);
+
    );
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
    die('Erro relacionado coa base de datos.');
 +
} catch (Throwable $e) {
  
−
try {
+
     die('Produciuse un erro na aplicación.');
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
+
}
−
    $conex->set_charset('utf8mb4');
 
  
−
$erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
 
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<!DOCTYPE html>
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
<html lang="gl">
 +
 
 
     <head>
 
     <head>
−
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
 
−
         <title>Operacións BD</title>
+
         <meta charset="UTF-8">
−
         <script type="text/javascript">
+
         <title>Buscar libros por autor</title>
−
             function abrirVentana($url){
+
         <script>
−
                 window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
+
            /*
 +
            * Recibe a URL que queremos abrir
 +
            * e solicita ao navegador unha nova xanela.
 +
            */
 +
             function abrirVentana(url) {
 +
 
 +
                 window.open(
 +
                    url,
 +
                    '_blank',
 +
                    'popup,width=600,height=400,noopener'
 +
                );
 
             }
 
             }
 
         </script>
 
         </script>
−
       
 
 
     </head>
 
     </head>
  
−
     <body>
+
<body>
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
+
 
−
             Introduce o nome do autor a buscar:
+
     <h1>Buscar libros por autor</h1>
−
            <input type='text' size='50' maxlength="100" required name="txtAutor" value='<?php echo (isset($_POST['txtAutor']) ? htmlentities($_POST["txtAutor"]):""); ?>'/>
+
    <form method="post">
−
             <input type="submit" value="BUSCAR" />
+
 
−
            <input type='button' onclick="javascript:this.form['txtAutor'].value='';" value="LIMPAR" />
+
        <label for="txtAutor">
−
        <?php      
+
             Introduce o nome ou apelidos do autor:
−
        if (!empty($_POST['txtAutor'])){  // Premimos o botón de busca
+
        </label>
−
            $textoBuscar = filter_var($_POST['txtAutor'],FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES); // Eliminamos da cadea de busca todos os caracteres non permitidos
+
        <input
−
            if (strlen($textoBuscar)<3){  // É un exemplo de control de erros. Indico que o texto a buscar ten que ter a lo menos 3 caracteres
+
            type="text"
−
                $erro="<h2>A cadea a buscar non pode ser menor de 3 caracteres</h2>";
+
            id="txtAutor"
−
            }
+
            name="txtAutor"
−
             else {
+
            size="50"
−
                 $query=sprintf(CONSULTAR_LIBROS_POR_AUTOR,$conex->real_escape_string($textoBuscar));
+
            maxlength="100"
−
                 $result=$conex->query($query);
+
            required
−
                 if ($result->num_rows>0){
+
            value="<?= htmlspecialchars(
−
                    $autor=''; // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
+
                        $textoBuscar,
−
                    $last_autor=''; // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
+
                        ENT_QUOTES | ENT_SUBSTITUTE,
−
                    echo "<table border='1'>";
+
                        'UTF-8'
−
                    echo "<th>Autor</th><th>Libro</th>";
+
                    ) ?>">
−
                    while($row = $result->fetch_assoc()) {
+
        <button
−
                        if ($last_autor==$row["nome_autor"]){
+
             type="submit"
−
                            $autor='';  // O autor é o mesmo e non queremos que se repita na táboa
+
            value="BUSCAR">Buscar</button>
−
                        }
+
        <input
−
                        else {
+
            type="reset"
−
                            $autor=htmlentities($row["nome_autor"]);
+
            value="LIMPAR">
−
                        }
+
 
−
                        echo '<tr>';
+
    </form>
−
                        $onclick=sprintf("javascript:abrirVentana(\"detalle_libro.php?libro=%d\");",htmlentities($row["id"]));
+
 
−
                        printf("<td>%s</td><td><a href='#' onclick='%s'>%s</a></td>",$autor,$onclick,htmlentities($row['titulo']));
+
    <?php if ($erro !== ''): ?>
−
                        echo '</tr>';
+
        <p>
−
                        $last_autor=$row["nome_autor"];
+
 
−
                    }
+
             <?= htmlspecialchars(
−
                    $result->free();
+
                 $erro,
−
                    echo "</table>";
+
                 ENT_QUOTES | ENT_SUBSTITUTE,
 +
                 'UTF-8'
 +
            ) ?>
 +
        </p>
 +
    <?php endif; ?>
 +
 
 +
    <?php if ($libros): ?>
 +
        <table border="1">
 +
            <tr>
 +
                <th>Autor</th>
 +
                <th>Libro</th>
 +
            </tr>
 +
            <?php
 +
 
 +
        /*
 +
        * Esta variable permite saber cal foi
 +
        * o último autor que amosamos.
 +
        */
 +
            $ultimoAutor = '';
 +
            foreach ($libros as $libro):
 +
            /*
 +
            * Se o autor desta fila é o mesmo
 +
            * que o da fila anterior,
 +
            * non volvemos amosar o seu nome.
 +
            */
 +
                if (
 +
                    $ultimoAutor ===
 +
                    $libro['nome_autor']
 +
                ) {
 +
 
 +
                    $autorAmosar = '';
 +
                } else {
 +
 
 +
                    $autorAmosar =
 +
                        $libro['nome_autor'];
 
                 }
 
                 }
−
                 else {
+
 
−
                     $erro = "<br />Non hai resultados coa cadea $textoBuscar";
+
            ?>
−
                }
+
                 <tr>
−
               
+
                     <td>
−
               
+
                        <?= htmlspecialchars(
−
            }
+
                            $autorAmosar,
−
        }
+
                            ENT_QUOTES | ENT_SUBSTITUTE,
−
       
+
                            'UTF-8'
−
        ?>      
+
                        ) ?>
−
        </form>
+
                    </td>
−
        <?php      
+
                    <td>
−
        if ($erro!='') {
+
                        <a
−
             printf("<h3>%s</h3>",$erro);
+
                            href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>"
−
        }
+
                            onclick="abrirVentana(this.href); return false;">
−
        ?>      
+
 
−
       
+
                            <?= htmlspecialchars(
−
       
+
                                $libro['titulo'],
−
    </body>  
+
                                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                                'UTF-8'
 +
                            ) ?>
 +
                        </a>
 +
                    </td>
 +
                </tr>
 +
            <?php
 +
 
 +
            /*
 +
            * Gardamos o autor actual para comparalo
 +
            * co autor da seguinte fila.
 +
            */
 +
                $ultimoAutor =
 +
                    $libro['nome_autor'];
 +
 
 +
            endforeach;
 +
             ?>
 +
        </table>
 +
    <?php endif; ?>
 +
</body>
 
</html>
 
</html>
 +
</syntaxhighlight>
  
−
<?php
+
===== Explicación da solución =====
−
}  // Pecha o try da parte superior da páxina
+
 
−
catch (mysqli_sql_exception $e) {
+
* Recuperamos o texto enviado polo formulario mediante:
−
    die("Erro mysql: " . $e->getMessage());
+
 
−
}
+
::<syntaxhighlight lang="php">
−
catch (Expcetion $e) {
+
$textoBuscar = trim(
−
    die("Erro xeral: " . $e->getMessage());
+
$_POST['txtAutor'] ?? ''
−
}
+
); </syntaxhighlight>
−
finally{
+
 
−
  if(!empty($conex)){
+
* Non empregamos `htmlspecialchars()` neste momento.
−
      $conex->close();
+
 
−
  }
+
: Neste punto estamos '''validando e procesando un dato de entrada'''.
−
}
+
 
−
?>
+
: `htmlspecialchars()` empregarase posteriormente cando o dato se introduza dentro do HTML.
−
</syntaxhighlight>
+
 
 +
 
 +
<br />
 +
'''Validación do dato'''
  
−
* Fixarse que nesta páxina non necesitamos conectar ao principio, xa que soamente se carga a caixa de texto para que o usuario introduza o nome do autor a buscar.
+
* Comprobamos primeiro que a cadea non estea baleira:
−
:: Polo tanto a conexión e o peche da base de datos se podería poñer dentro do else da liña 43.
 
−
* Fixarse que o botón de limpar non é de tipo 'reset' xa que ao cargar o que veña en $_POST o botón tipo reset volvería a amosar dito valor.
 
  
 +
::<syntaxhighlight lang="php">
 +
if ($textoBuscar === '') { </syntaxhighlight>
  
−
* '''Resultado:'''
+
* Como exemplo dunha regra de validación da aplicación, esiximos tamén unha lonxitude mínima:
−
[[Imagen:Php_BD_11.jpg|500px|center]]
 
  
 +
::<syntaxhighlight lang="php">
 +
elseif (mb_strlen($textoBuscar) < 3) { </syntaxhighlight>
  
 +
: A esixencia de tres caracteres é unha regra da nosa aplicación.
  
 +
: Non é unha medida para evitar inxección SQL.
  
  
  
 
<br />
 
<br />
 +
'''Preparación da consulta'''
 +
 +
* A consulta recibe datos procedentes do usuario, polo que debemos empregar unha sentenza preparada:
 +
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTAR_LIBROS_POR_AUTOR
 +
); </syntaxhighlight>
 +
 +
 +
<br />
 +
'''Asociación do parámetro'''
 +
 +
* Asociamos o valor ao parámetro `:autor`:
 +
 +
::<syntaxhighlight lang="php">
 +
$stmt->bindValue(
 +
':autor',
 +
'%' . $textoBuscar . '%',
 +
PDO::PARAM_STR
 +
); </syntaxhighlight>
 +
 +
* Indicamos:
  
−
=== Consulta chamando a procedementos almacenados ===
+
::<syntaxhighlight lang="php">
 +
PDO::PARAM_STR </syntaxhighlight>
  
−
* Un procedemento almacenado ven a ser o equivalente a unha función en PHP, coa diferenza que as ordes que conforman o procedemento van permitir realizar accións (inserción, borrado, modificación e selección) sobre a base de datos.
+
: porque estamos enviando unha cadea.
  
−
* Como comentamos [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Soluci.C3.B3n_a_inxecci.C3.B3n_de_CSS anteriormente nesta wiki], unha das mellores formas de evitar ataques de tipo 'inxección de CSS' é utilizar procedementos almacenados.
 
  
−
: Tamén é unha forma de aumentar a seguridade de acceso á base de datos Mysql, xa que teremos que dar permiso de execución sobre o procedemento almacenado e o usuario non vai poder acceder ás táboas da base de datos nin examinar a estrutura das mesmas.
 
−
: Desta forma podemos cambiar a estrutura da táboa (como por exemplo cambiar o nome dunha columna) e simplemente cambiando a sentenza SQL que está dentro do procedemento almacenado e poñendo un alias có nome antigo, a aplicación no cliente segue funcionando.
 
−
: Outra vantaxe é que as sentencias SQL non están definidas nas páxinas PHP e polo tanto se realizamos un cambio o faremos no Mysql sendo 'transparente' cara ao usuario.
 
  
 +
<br />
 +
'''Funcionamento de LIKE'''
  
−
* Máis información [http://php.net/manual/es/mysqli.quickstart.stored-procedures.php neste enlace].
+
* Os caracteres `%` actúan como comodíns.
  
−
* O primeiro que temos que facer é crear o procedemento almacenado.
+
: Se o usuario introduce:
−
* A orde SQL é [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html '''CREATE PROCEDURE'''].
 
  
−
* Creamos o procedemento almacenado:
+
::<syntaxhighlight lang="text">
−
:* Cando definimos un procedemento almacenado debemos facer uso da palabra DELIMITER para indicarlle ao Mysql onde acaba a definición do procedemento almacenado. Máis información [http://dev.mysql.com/doc/refman/5.7/en/stored-programs-defining.html neste enlace].
+
gar </syntaxhighlight>
  
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
+
: PHP enviará:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `NOME_PROCEDEMENTO` ()
+
::<syntaxhighlight lang="text">
−
BEGIN
+
%gar% </syntaxhighlight>
  
−
  select col1,col2
+
* Desta forma poderán coincidir cadeas nas que `gar` apareza ao principio, no medio ou ao final.
−
  from TABOA_1
 
−
  order by col1;
 
  
  
−
END
+
<br />
−
$$
+
'''Execución e recuperación dos resultados'''
−
</syntaxhighlight>  
+
 
 +
* Executamos:
  
−
: Como vemos o procedemento está formado por unha consulta SELECT (pode poñerse calquera)
+
::<syntaxhighlight lang="php">
 +
$stmt->execute(); </syntaxhighlight>
  
 +
* E recuperamos os resultados:
  
−
* Agora dende PHP temos que facer uso do comando CALL 'NOME_PROCEDEMENTO'.
+
::<syntaxhighlight lang="php">
−
: Isto é o que teriamos que poñer se dende Mysql queredes chamar ao procedemento....
+
$libros = $stmt->fetchAll(); </syntaxhighlight>
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="12" >
+
* Como na conexión configuramos:
−
<?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.inc.php');  // Onde están definidas as constantes que se empregan na conexión (feito nas seccións anteriores)
+
::<syntaxhighlight lang="php">
 +
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC </syntaxhighlight>
  
−
try {
+
: cada fila recuperada será un array asociativo.
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
−
    $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
  
−
} 
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
  
 +
<br />
 +
'''Evitar repetir o nome do autor'''
  
−
</syntaxhighlight>
+
* Pode suceder que un mesmo autor teña varios libros.
  
 +
: Por exemplo:
  
 +
::<syntaxhighlight lang="text">
 +
García, Ana | Libro A
 +
García, Ana | Libro B
 +
García, Ana | Libro C
 +
Pérez, Luís | Libro D </syntaxhighlight>
  
−
* A diferenza do caso anterior, cando chamemos a un procedemento almacenado teremos que facer uso dunha das seguintes funcións:
+
* Queremos amosalo desta forma:
−
:: [http://php.net/manual/es/mysqli.real-query.php mysqli::real_query] ou [http://php.net/manual/es/mysqli.multi-query.php mysqli::multi_query]
 
  
−
: Nos imos facer uso da chamada a '''multi_query''' xa que tamén a imos poder utilizar cando dentro dun mesmo procedemento almacenado teñamos varios select´s e queiramos recuperar o contido de todos eles (cada un será un conxunto de resultados).
+
::<syntaxhighlight lang="text">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,4" >
+
García, Ana | Libro A
−
        ........................
+
            | Libro B
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
+
             | Libro C
−
        if ($conex->multi_query($proc)){
+
Pérez, Luís | Libro D </syntaxhighlight>
−
             $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
  
−
        }
+
* Para iso gardamos o último autor procesado:
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
 +
$ultimoAutor = ''; </syntaxhighlight>
  
 +
* En cada iteración comparamos:
  
−
: Unha vez temos o conxunto de resultados ($result) xa operamos como nos casos anteriores.
+
::<syntaxhighlight lang="php">
 +
if ($ultimoAutor ===$libro['nome_autor']) {
  
 +
  $autorAmosar = '';
  
−
*<u>IMPORTANTE</u> Se estamos a utilizar procedementos almacenados, pode darse o caso de que despois de chamar a un procedemento que devolva <u>un conxunto de resultados</u>, ao intentar chamar a outro procedemento vos dea un erro '2014 - Commands out of sync; you can't run this command now' ou simplemente a chamada ao seguinte procedemento non vos devolva resultados.
+
} else {
−
: A resposta a ese erro a tedes [https://dev.mysql.com/doc/refman/8.0/en/commands-out-of-sync.html neste enlace].
+
    $autorAmosar = $libro['nome_autor'];
 +
}
 +
</syntaxhighlight>
  
 +
* E ao rematar gardamos o autor actual:
  
 +
::<syntaxhighlight lang="php">
 +
$ultimoAutor =
 +
$libro['nome_autor']; </syntaxhighlight>
  
−
* Ao parecer, existen aínda conxuntos de resultados sen 'recoller' (aínda que o procedemento soamente devolva un) e non vos deixará realizar outra chamada ata que leades todos os conxuntos de resultados.
 
−
: Para facelo, podedes engadir o seguinte código:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="9" >
 
−
        ........................
 
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
                // Procesamos cada fila
 
−
            }
 
−
            while($conex->next_result() && $conex->store_result());
 
  
−
        }
+
<br />
 +
'''Abrir o detalle dun libro'''
  
−
</syntaxhighlight>
+
* Cada título será un enlace á páxina:
  
 +
::<syntaxhighlight lang="text">detalle_libro.php </syntaxhighlight>
  
 +
* O identificador enviarase mediante `GET`:
  
 +
::<syntaxhighlight lang="text">detalle_libro.php?libro=5 </syntaxhighlight>
  
−
* Como comentei anteriormente, un procedemento pode estar conformado por múltiples 'select'.
 
−
: Se queremos 'pasar' ao seguinte 'conxunto de resultados' temos que chamar á [http://php.net/manual/es/mysqli.next-result.php función mysqli::next_result] desta forma:
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="8-10" >
 
−
        ........................
 
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
            if ($conex->next_result()){
 
−
              $result=$conex->store_result();
 
−
            }
 
  
−
        }
+
* O enlace xerado será:
 +
::<syntaxhighlight lang="php">
 +
<a href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>" onclick="abrirVentana(this.href); return false;">
 +
</syntaxhighlight>
  
−
</syntaxhighlight>
+
* Mediante:
  
−
: Se imos procesar todos os conxuntos de resultados da mesma forma poderíamos utilizar un while{}...
+
::<syntaxhighlight lang="javascript">this.href </syntaxhighlight>
  
 +
: obtemos a URL real almacenada no atributo `href`.
  
 +
* Chamamos despois á función:
  
 +
::<syntaxhighlight lang="javascript">abrirVentana(this.href); </syntaxhighlight>
  
−
<br />
+
* Finalmente:
−
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
 
  
−
* Vexamos un exemplo. Imos copiar o código da páxina '''Consultar_Libros.php''' e imos crear unha nova de nome '''Consultar_Libros_Proc''' co mesmo código, facendo as modificacións que se indican a continuación para que faga uso da chamada a un procedemento almacenado.
+
::<syntaxhighlight lang="javascript">return false; </syntaxhighlight>
  
 +
: evita que o navegador abra tamén o enlace na xanela actual.
  
−
* Creamos o procedemento almacenado:
 
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS` ()
 
−
BEGIN
 
  
−
  select ìd,titulo
+
<br />
−
  from libros
+
'''Función JavaScript'''
−
  order by titulo;
 
  
 +
* A función empregada é:
  
−
END
+
::<syntaxhighlight lang="javascript">
−
$$
+
function abrirVentana(url) {
−
</syntaxhighlight>  
 
  
 +
window.open(
 +
    url,
 +
    '_blank',
 +
    'popup,width=600,height=400,noopener'
 +
);
  
 +
} </syntaxhighlight>
  
−
* Definimos nunha constante o nome do procedemento almacenado dentro do arquivo bd.inc.php.
+
* `window.open()` solicita ao navegador a apertura dunha nova xanela.
  
−
:'''Arquivo: bd.inc.php'''
+
* Indicamos aproximadamente:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
::<syntaxhighlight lang="text">
−
define('PROCEDEMENTO_CONSULTAR_LIBROS','CONSULTAR_LIBROS()');  // Se levara parámetros faríamos o mesmo que nos casos anteriores coas sentencias select, empregando %s, %d...
+
width=600
−
</syntaxhighlight>  
+
height=400 </syntaxhighlight>
  
 +
* A opción:
 +
::<syntaxhighlight lang="text">
 +
noopener </syntaxhighlight>
 +
: evita que a nova xanela poida acceder á xanela orixinal mediante `window.opener`.
  
 +
'''NOTA:''' O navegador ten a decisión final sobre se a páxina se abre nunha nova xanela ou nunha pestana e pode ignorar algunhas características solicitadas.
  
−
* Debemos cambiar a chamada a 'query' pola chamada [http://php.net/manual/es/mysqli.real-query.php mysqli::real_query] ou [http://php.net/manual/es/mysqli.multi-query.php mysqli::multi_query]
 
−
:: Nos imos facer uso da chamada a '''multi_query''' xa que tamén a imos poder utilizar cando dentro dun mesmo procedemento almacenado teñamos varios select´s e queiramos recuperar o contido de todos eles (cada un será un conxunto de resultados).
 
  
−
:'''Arquivo: consultar_libros_proc.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="24-26" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
<br />
 +
'''Vantaxe de empregar un href real'''
  
−
try {
+
* Non empregamos:
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
+
::<syntaxhighlight lang="html"> <a href="#"> </syntaxhighlight>
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
* Empregamos:
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
::<syntaxhighlight lang="html"> <a href="detalle_libro.php?libro=5"> </syntaxhighlight>
−
    "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>Operacións BD</title>
 
−
    </head>
 
−
    <body>
 
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
 
−
        <?php       
 
−
        $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_LIBROS);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<select name='lstLibros' onchange='this.form.submit()'>";
 
−
                echo "<option value='-1'></option>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    if(isset($_POST['lstLibros']) && $row['id']==$_POST['lstLibros']){
 
−
                        $cadea="selected='selected'";
 
−
                    }
 
−
                    else{
 
−
                        $cadea='';
 
−
                    }
 
−
                    printf("<option value='%d' %s>%s</option>",$row["id"],$cadea,$row["titulo"]);
 
−
                }
 
−
                echo '</select>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar...";
 
−
            }
 
−
               
 
−
        }
 
−
        else {
 
−
            echo "Houbo un erro na chamado ao procedemento:" . $conex->error; // A mensaxe do erro ($conex->error) non a teremos que amosar coa aplicación funcionando
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
  </body>   
 
−
</html>
 
−
<?php
 
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>  
 
  
−
:* Liña 24: Creamos a cadea que vai chamar ao procedemento almacenado: CALL CONSULTAR_LIBROS()
+
: Desta forma, se JavaScript non funciona ou está desactivado, o enlace segue sendo válido e o usuario poderá acceder igualmente ao detalle do libro.
−
:* Liña 25: Modificamos a liña e agora chamamos ao procedemento mysqli::multi_query(nome_proc). Dita chamada devolve false en caso de erro.
 
−
:* Liña 26: Gardamos o resultado (o primeiro select do procedemento almacenado) no obxecto $result e xa operamos igual que no caso anterior cando usamos as sentencias 'select' directamente.
 
  
  
  
 
<br />
 
<br />
−
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
+
'''Páxina detalle_libro.php'''
 +
 
 +
* A páxina de detalle recibirá o identificador do libro mediante:
 +
 
 +
::<syntaxhighlight lang="text">$_GET['libro'] </syntaxhighlight>
 +
 
 +
* Como é un dato externo, debemos validalo:
  
−
* Podemos chamar a un procedemento almacenado enviando datos ao mesmo en forma de parámetros.
+
::<syntaxhighlight lang="php">
−
: Indicar que en case todos os xestores os parámetros poden ser de 3 tipos: '''de entrada''', '''de saída''' e de '''entrada/saída'''.
+
$libroId = filter_input(
−
::* Os <u>parámetros de saída</u> (OUT) son variables que van poder ser modificadas no interior do procedemento e cuxo valor vai poder ser lido dentro do código da páxina php.
+
INPUT_GET,
−
::* Os <u>parámetros de entrada</u> (IN) son variables que van poder ser utilizadas dentro do procedemento (chegan en forma de parámetros) pero as modificacións que se fagan no seu valor non chegan á paxina PHP.
+
'libro',
−
::* Os <u>parámetros de entrada/entrada</u> (INOUT) son unha combinación dos dous anteriores.
+
FILTER_VALIDATE_INT
 +
);
  
−
: Máis información [http://www.mysqltutorial.org/stored-procedures-parameters.aspx neste enlace].
+
if (
 +
  $libroId === false ||
 +
  $libroId === null ||
 +
  $libroId < 1
 +
) {
  
−
* Por defecto, se non especificamos nada, todos os parámetros dun procedemento almacenado son considerados 'de entrada'
+
  die('Identificador de libro non válido.');
  
−
* Primeiro teremos que definir o procedemento en Mysql:
+
}
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
</syntaxhighlight>
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100))
 
−
BEGIN
 
  
−
    SELECT col1,col2
 
−
    FROM TABOA_1
 
−
    WHERE col2=_param1;
 
  
−
END
+
* A consulta posterior tamén deberá ser preparada:
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
  'SELECT id, titulo
 +
  FROM libros
 +
  WHERE id = :libro_id'
 +
);
  
−
: Como vemos, xunto ao nome do procedemento hai que especificar o tipo (dentro dos tipos de mysql).
+
$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
−
: Unha vez definidos podemos utilizalos dentro do procedemento almacenado.
+
$stmt->execute();
−
: Calquera modificación do valor do parámetro non se verá reflectidos no código PHP. Se no código PHP 'recuperamos' o valor do parámetro, este terá o mesmo valor que cando enviamos o dato ao procedemento.
 
  
 +
$libro = $stmt->fetch();
 +
</syntaxhighlight>
  
−
: Podemos definir máis parámetros, separando por comas cada un deles, por exemplo:
+
* Se queremos recuperar tamén os exemplares dese libro faremos outra consulta preparada:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,8,9" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100),_param2 int)
+
::<syntaxhighlight lang="php">
−
BEGIN
+
$stmt = $conex->prepare(
 +
'SELECT id_exemplar,imaxe,prezo,estado_id
 +
  FROM exemplares
 +
  WHERE libro_id = :libro_id
 +
  ORDER BY id_exemplar'
 +
);
  
−
    SELECT col1,col2
+
$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
−
    FROM TABOA_1
+
$stmt->execute();
−
    WHERE col2=_param1 and
 
−
          col1=_param2
 
  
−
END
+
$exemplares = $stmt->fetchAll();
−
</syntaxhighlight>  
+
</syntaxhighlight>
  
 +
===== Resumo do proceso =====
  
−
* Unha  vez definido teremos que enviar os datos que queiramos dende PHP desta forma:
+
::<syntaxhighlight lang="text">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
$_POST['txtAutor']
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
+
↓
−
</syntaxhighlight>
+
trim()
−
: Fixarse como no caso de datos de tipo numérico non enviamos o dato con comiñas.
+
↓
−
: Fixarse como 'xogamos' coas comiñas simples de dobres para pechar correctamente a cadea.
+
validación
 +
↓
 +
prepare()
 +
↓
 +
bindValue(PDO::PARAM_STR)
 +
↓
 +
execute()
 +
↓
 +
fetchAll()
 +
↓
 +
htmlspecialchars()
 +
↓
 +
enlace ao libro
 +
↓
 +
window.open()
 +
↓
 +
detalle_libro.php?libro=ID
 +
↓
 +
FILTER_VALIDATE_INT
 +
↓
 +
prepare() + PDO::PARAM_INT </syntaxhighlight>
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="5,6" >
+
</syntaxhighlight>
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
 
  
−
        ........................
 
  
−
        $proc = sprintf(CHAMAR_PROCEDEMENTO_CON_PARAMETROS,valor_numérico,'valor_cadea');
+
* Debemos diferenciar claramente:
−
        $proc = sprintf("CALL %s",$proc);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
        }
 
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="text">
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
+
! Operación
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
  
 +
| ! Finalidade                                          |
 +
| ------------------------------------------------------ |
 +
| `trim()` e comprobación da lonxitude                  |
 +
| Validar os datos segundo as regras da aplicación.      |
 +
| -                                                      |
 +
| `prepare()` + `bindValue()`                            |
 +
| Separar os datos da estrutura da sentenza SQL.        |
 +
| -                                                      |
 +
| `PDO::PARAM_STR`                                      |
 +
| Indicar que o parámetro enviado é unha cadea.          |
 +
| -                                                      |
 +
| `htmlspecialchars()`                                  |
 +
| Escapar datos cando se introducen dentro de HTML.      |
 +
| -                                                      |
 +
| `window.open()`                                        |
 +
| Abrir o detalle do libro nunha nova xanela ou pestana. |
 +
| -                                                      |
 +
| `FILTER_VALIDATE_INT`                                  |
 +
| Validar o identificador recibido por `GET`.            |
 +
| }                                                      |
 +
</syntaxhighlight>
  
  
  
 
<br />
 
<br />
−
==== Caso Práctico: Consulta chamando a procedementos almacenados con parámetros de entrada ====
 
  
−
* Seguindo có noso exemplo...imos crear unha nova páxina de nome '''buscar_libros_por_autor_proc.php''' que faga uso de procedementos almacenados en vez de sentencias SQL directamente no código.
+
=== Consulta chamando a procedementos almacenados ===
 +
 
 +
* Un procedemento almacenado permite gardar no propio xestor de base de datos un conxunto de sentenzas SQL que poden ser executadas posteriormente.
 +
 
 +
* Desde PDO chamarémolo mediante a sentenza:
  
−
'''Solución:'''
+
::<syntaxhighlight lang="sql">
−
* '''Procedemento almacenado CONSULTAR_LIBROS_POR_AUTOR en Mysql'''
+
CALL NOME_PROCEDEMENTO() </syntaxhighlight>
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
 
 +
* Por exemplo, en MySQL:
 +
 
 +
::<syntaxhighlight lang="sql">
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS_POR_AUTOR`(IN p_autor_buscar varchar(100))
+
CREATE PROCEDURE CONSULTAR_LIBROS()
 
BEGIN
 
BEGIN
  
−
select CONCAT(apelidos,',',nome) as nome_autor,libros.id as id_libro,titulo from libros
+
SELECT id, titulo
−
            INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id)
+
FROM libros
−
            INNER JOIN autores ON (libro_autor.autor_id=autores.id)
+
ORDER BY titulo;
−
where CONCAT(autores.nome,' ',autores.apelidos) like  CONCAT('%', p_autor_buscar , '%')
 
−
order by nome_autor,titulo;
 
  
 
END$$
 
END$$
−
</syntaxhighlight>
 
  
−
: <u>Nota:</u> Para comprobar se funciona facede primeiro unha chamada dende o propio Mysql coa orde: call CONSULTAR_LIBROS_POR_AUTOR('dru');
+
DELIMITER ; </syntaxhighlight>
−
: Fixarse como o parámetro é de tipo IN (valor por defecto, podería non poñerse nada).
+
 
 +
==== Empregando PDO ====
 +
 
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_LIBROS()'
 +
);
 +
 
 +
$stmt->execute();
 +
 
 +
$libros = $stmt->fetchAll();
 +
 
 +
$stmt->closeCursor(); </syntaxhighlight>
  
 +
* `closeCursor()` libera os resultados pendentes asociados á chamada.
  
 +
: É especialmente recomendable ao traballar con procedementos almacenados en MySQL antes de executar outra consulta na mesma conexión.
  
−
* Definimos a constante en PHP no que está definido o nome do procedemento e o parámetro que vai levar.
+
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
−
:'''Arquivo: bd.inc.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_POR_AUTOR','CONSULTAR_LIBROS_POR_AUTOR("%s")');
 
−
</syntaxhighlight>
 
  
−
:<u>Nota:</u> Fixarse como o parámetro ao ser de tipo 'cadea' debe ir entre comillas.
+
* Definimos o nome do procedemento:
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'PROCEDEMENTO_CONSULTAR_LIBROS',
 +
'CONSULTAR_LIBROS'
 +
); </syntaxhighlight>
  
 +
* Realizamos a chamada:
  
−
* '''Arquivo buscar_libros_por_autor_proc.php'''
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="44-47" >
+
$stmt = $conex->prepare(
−
<?php
+
'CALL ' .
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
PROCEDEMENTO_CONSULTAR_LIBROS .
−
ini_set('display_errors', 1);
+
'()'
−
ini_set('display_startup_errors', 1);
+
);
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
$stmt->execute();
  
−
try {
+
$libros = $stmt->fetchAll();
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
    $erro="";   // Variable que a usaremos para amosar os posibles erros que poidan darse
+
$stmt->closeCursor(); </syntaxhighlight>
  
−
?>
+
* Non necesitamos comprobar o resultado de `execute()` para saber se o `SELECT` devolveu filas.
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
: `execute()` indica se a sentenza puido executarse.
−
    "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>Operacións BD</title>
 
−
        <script type="text/javascript">
 
−
            function abrirVentana($url){
 
−
                window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
 
−
            }
 
−
        </script>
 
−
       
 
−
    </head>
 
  
−
    <body>
+
: Para saber se temos resultados comprobamos os datos recuperados:
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
 
−
            Introduce o nome do autor a buscar:
 
−
            <input type='text' size='50' maxlength="100" required name="txtAutor" value='<?php echo (isset($_POST['txtAutor']) ? $_POST["txtAutor"]:""); ?>'/>
 
−
            <input type="submit" value="BUSCAR" />
 
−
            <input type='button' onclick="javascript:this.form['txtAutor'].value='';" value="LIMPAR" />
 
−
        <?php       
 
−
        if (!empty($_POST['txtAutor'])){  // Premimos o botón de busca
 
−
            $textoBuscar = filter_var($_POST['txtAutor'],FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES); // Eliminamos da cadea de busca todos os caracteres non permitidos
 
−
            if (strlen($textoBuscar)<3){  // É un exemplo de control de erros. Indico que o texto a buscar ten que ter a lo menos 3 caracteres
 
−
                $erro="<h2>A cadea a buscar non pode ser menor de 3 caracteres</h2>";
 
−
            }
 
−
            else {
 
−
                $proc = sprintf(PROCEDEMENTO_CONSULTAR_LIBROS_POR_AUTOR,$conex->real_escape_string($textoBuscar)); // Cambiamos o texto a buscar polo parámetro definido na constante
 
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();
 
−
                    if ($result->num_rows>0){
 
−
                        $autor='';  // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                        $last_autor=''; // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                        echo "<table border='1'>";
 
−
                        echo "<th>Autor</th><th>Libro</th>";
 
−
                        while($row = $result->fetch_assoc()) {
 
−
                            if ($last_autor==$row["nome_autor"]){
 
−
                                $autor='';
 
−
                            }
 
−
                            else {
 
−
                                $autor=$row["nome_autor"];
 
−
                            }
 
−
                            echo '<tr>';
 
−
                            $onclick=sprintf("javascript:abrirVentana(\"detalle_libro.php?libro=%d\");",htmlentities($row["id_libro"]));
 
−
                            printf("<td>%s</td><td><a href='#' onclick='%s'>%s</a></td>",$autor,$onclick,htmlentities($row['titulo']));
 
−
                            echo '</tr>';
 
−
                            $last_autor=$row["nome_autor"];
 
−
                        }
 
−
                        $result->free();
 
−
                        echo "</table>";
 
−
                    }
 
−
                    else {
 
−
                        $erro = "Non hai resultados,";
 
−
                    }
 
−
                }
 
−
            }
 
−
        }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
        <?php       
 
−
        if ($erro!='') {
 
−
            printf("<h3>%s</h3>",$erro);
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
::<syntaxhighlight lang="php">
−
}    // Pecha o try do principio da páxina
+
if ($libros) {
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
−
:* Liñas 44 e 45: Creamos a cadea que vai chamar ao procedemento almacenado de Mysql: CALL CONSULTAR_LIBROS_POR_AUTOR('dru'); (é un exemplo de busca pola palabra dru)
 
  
 +
// Hai resultados
  
 +
} else {
  
 +
// Non hai resultados
  
 +
} </syntaxhighlight>
  
 
<br />
 
<br />
−
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
 
  
−
* Agora imos aplicar o concepto de parámetros de saída, que como indicamos no punto anterior, son parámetros que van poder ser modificados o seu valor dentro do procedemento almacenado e recoller o seu valor modificado dende a parte de PHP.
+
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
 +
 
 +
* Os parámetros dun procedemento almacenado poden ser:
 +
 
 +
:* `IN`: parámetros de entrada.
 +
:* `OUT`: parámetros de saída.
 +
:* `INOUT`: funcionan como entrada e saída.
 +
 
 +
* Por exemplo:
  
−
* Primeiro teremos que definir o procedemento en Mysql:
+
::<syntaxhighlight lang="sql">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,7" >
 
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(OUT _param1 varchar(100))
+
CREATE PROCEDURE PROCEDEMENTO_CON_PARAMETROS(
 +
IN p_num INT,
 +
IN p_text VARCHAR(100)
 +
)
 
BEGIN
 
BEGIN
  
−
    SELECT col2
+
SELECT col1, col2
−
    INTO _param1
+
FROM TABOA_1
−
    FROM TABOA_1
+
WHERE col1 = p_num
−
    WHERE col1=1;
+
  AND col2 = p_text;
  
−
END
+
END$$
−
</syntaxhighlight>  
+
 
 +
DELIMITER ; </syntaxhighlight>
 +
 
 +
==== Empregando PDO ====
  
−
: Agora especificamos que o parámetro é de saída antepoñendo a palabra OUT ao seu nomo.
+
* Preparamos a chamada igual que calquera outra sentenza parametrizada:
−
: Neste exemplo estamos a supoñer que a consulta devolve unha única fila e estamos gardando o resultado da consulta no parámetro de saída.
 
−
: Podemos modificar o seu valor dentro do procedemento coa orde: SET _param1=valor;
 
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL PROCEDEMENTO_CON_PARAMETROS(
 +
:param_num,
 +
:param_text
 +
)'
 +
);
  
 +
$stmt->bindValue(
 +
':param_num',
 +
$valorNum,
 +
PDO::PARAM_INT
 +
);
  
−
* Unha  vez definido teremos que construír a chamada ao procedemento enviando o nome dunha variable (para recoller o valor modificado).
+
$stmt->bindValue(
−
: O nome da variable terá este formato: '''@'''nomeVariable:
+
':param_text',
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
$valorTexto,
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
+
PDO::PARAM_STR
−
</syntaxhighlight>
+
);
−
: Fixarse como a variable é de tipo cadea e vai entre comiñas.
 
  
 +
$stmt->execute();
  
−
* Agora a chamada se fai igual que no caso anterior, pero fixarse que neste caso O PROCEDEMENTO NON DEVOLVE UN CONXUNTO DE RESULTADOS. Podería ser e nese caso obraríamos igual que no caso anterior, a maiores do que imos ver agora.
+
$filas = $stmt->fetchAll();
−
: Para recoller o dato da variable teremos que facer unha consulta SELECT da forma: SELECT @nomeVariable as valorDevolto
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="10,14" >
+
$stmt->closeCursor(); </syntaxhighlight>
−
<?php
 
−
        define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
 
  
−
        ........................
+
* Desta forma non necesitamos empregar `sprintf()` nin concatenar os valores dentro da chamada SQL.
  
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
+
==== Caso Práctico: buscar libros por autor ====
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
  
−
            if (!($resultado = $conex->query("SELECT @nomeVariable as valorDevolto"))) {
+
* Lembrar que o usuario que se conecta á base de datos dende php ten que ter permiso execute sobre a base de datos.
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
+
::<syntaxhighlight lang="sql">
−
            }
+
grant execute on php.* to 'user_php'@'localhost';
−
            else {
+
flush privileges;
−
                $fila = $resultado->fetch_assoc();
+
</syntaxhighlight>
−
                echo $fila['valorDevolto']);
 
−
            }
 
  
−
        }
 
−
        ........................
 
  
 +
* Procedemento almacenado (isto tedes que executalo en Mysql empregando a ferramenta Mysql WorkBench ou PhpMyAdmin):
  
−
?>
+
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
−
</syntaxhighlight>
+
CREATE PROCEDURE CONSULTAR_LIBROS_POR_AUTOR(
 +
IN p_autor_buscar VARCHAR(100)
 +
)
 +
BEGIN
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor') // 5 e valor son datos de exemplo
+
SELECT CONCAT(
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
+
          autores.apelidos,
 +
          ', ',
 +
          autores.nome
 +
      ) AS nome_autor,
 +
      libros.id AS id_libro,
 +
      libros.titulo
 +
FROM libros
 +
INNER JOIN libro_autor
 +
    ON libro_autor.libro_id = libros.id
 +
INNER JOIN autores
 +
    ON libro_autor.autor_id = autores.id
 +
WHERE CONCAT(
 +
          autores.nome,
 +
          ' ',
 +
          autores.apelidos
 +
      ) LIKE CONCAT(
 +
          '%',
 +
          p_autor_buscar,
 +
          '%'
 +
      )
 +
ORDER BY nome_autor, libros.titulo;
  
 +
END$$
  
 +
DELIMITER ; </syntaxhighlight>
  
−
: <u>Nota:</u> Non fai falla facer un alias do nome da variable. Podemos empregar o propio nome no fetch_assoc().
+
* Chamada desde PDO cambiando no exercicio anterior a chamada ao SELECT pola chamada a este procedemento almacenado:
−
:: Podemos poñer:  
 
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="5,10" >
+
::<syntaxhighlight lang="php" highlight="23-25,58">
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
+
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
  
−
            if (!($resultado = $conex->query("SELECT @nomeVariable"))) {
+
        /*
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
+
        * Comprobamos que se introduciu algún texto.
−
            }
+
        */
−
            else {
+
        if ($textoBuscar === '') {
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['@nomVariable']);
 
−
            }
 
  
−
        }
+
            $erro = 'Debes introducir un texto para realizar a busca.';
−
</syntaxhighlight>
 
  
 +
            /*
 +
        * Como regra da aplicación esiximos
 +
        * polo menos 3 caracteres.
 +
        */
 +
        } elseif (mb_strlen($textoBuscar) < 3) {
  
 +
            $erro =
 +
                'A cadea a buscar debe ter polo menos 3 caracteres.';
 +
        } else {
  
 +
            /*
 +
            * Preparamos a consulta.
 +
            */
 +
            $stmt = $conex->prepare(
 +
                'call CONSULTAR_LIBROS_POR_AUTOR(:autor)'
 +
            );
  
−
<br />
 
−
==== Caso práctico: Consulta chamando a procedementos almacenados con parámetros de saída ====
 
  
−
* Segundo o noso exemplo, imos crear un procedemento almacenado que devolva nun parámetro de saída o número de libros que temos na nosa tenda.
+
            /*
 +
            * Engadimos os comodíns % de LIKE ao valor
 +
            * que enviamos ao parámetro.
 +
            *
 +
            * Por exemplo:
 +
            *
 +
            * gar
 +
            *
 +
            * convértese en:
 +
            *
 +
            * %gar%
 +
            */
 +
            $stmt->bindValue(
 +
                ':autor',
 +
                '%' . $textoBuscar . '%',
 +
                PDO::PARAM_STR
 +
            );
  
  
−
* '''Procedemento almacenado CONSULTAR_NUM_LIBROS en Mysql'''
+
            /*
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
            * Executamos a consulta.
−
DELIMITER $$
+
            */
 +
            $stmt->execute();
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_NUM_LIBROS`(OUT p_num_libros int)
 
−
BEGIN
 
−
SELECT COUNT(*)
 
−
INTO p_num_libros
 
−
FROM libros;
 
  
−
END
+
            /*
−
$$
+
            * Recuperamos todos os resultados.
−
</syntaxhighlight>  
+
            */
 +
            $libros = $stmt->fetchAll();
  
 +
            $stmt->closeCursor();
  
−
: Como se pode ver leva un parámetro de saída (OUT). Indicar que podemos poñer calquera combinación de parámetros na definición dun procedemento almacenado.
 
  
 +
            /*
 +
            * Se non hai filas gardamos unha mensaxe
 +
            * para amosala posteriormente.
 +
            */
 +
            if (!$libros) {
  
−
* '''Arquivo bd.inc.php''':
+
                $erro =
−
* Definimos a chamada ao procedemento cunha variable no seu interior que vai ser a que garde o valor modificado de dentro do procedemento.
+
                    'Non se atoparon libros para ese autor.';
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
            }
−
define('PROCEDEMENTO_CONSULTAR_NUM_LIBROS','CONSULTAR_NUM_LIBROS(@numero_libros)');
+
        }
 +
    }
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
 +
 +
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
 +
 +
* Un parámetro `OUT` permite que un procedemento devolva un valor.
 +
 +
* Por exemplo:
 +
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
 +
CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
 +
OUT p_num_libros INT
 +
)
 +
BEGIN
  
−
* '''Arquivo consultar_num_libros.php'''
+
SELECT COUNT(*)
−
: Esta páxina está a modo de exemplo. Loxicamente isto podería ser utilizado dentro dunha páxina para obter o número de libros sen necesidade de crear unha páxina específica.
+
INTO p_num_libros
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
FROM libros;
−
<?php
+
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
END$$
−
ini_set('display_errors', 1);
+
 
−
ini_set('display_startup_errors', 1);
+
DELIMITER ; </syntaxhighlight>
−
error_reporting(E_ALL);
+
 
 +
* Con PDO e MySQL podemos empregar unha variable de usuario de MySQL:
  
−
require ('/var/www/confBD/bd.inc.php');
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_NUM_LIBROS(
 +
@numero_libros
 +
)'
 +
);
  
−
try {
+
$stmt->execute();
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
$erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
/*
−
?>
 
−
<!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>Operacións BD</title>
 
−
       
 
−
    </head>
 
  
−
    <body>
+
* É importante liberar os resultados
 +
* da chamada antes de executar outro SELECT.
 +
  */
 +
  $stmt->closeCursor();
  
−
        <?php
+
$stmt = $conex->query(
−
            $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
+
'SELECT @numero_libros'
−
            if ($conex->multi_query($proc)){
+
);
−
                $result = $conex->store_result();
 
−
                if (!($resultado = $conex->query("SELECT @numero_libros as _p_out"))) {
 
−
                    echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
                }
 
−
                else {
 
−
                    $fila = $resultado->fetch_assoc();
 
−
                    printf("<h1>NUMERO DE LIBROS A VENDA:%d",$fila['_p_out']);
 
−
                }
 
−
            }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
−
<?php
 
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
  
 +
* Podemos amosar o resultado:
  
 +
::<syntaxhighlight lang="php">
 +
printf(
 +
'<h1>Número de libros á venda: %d</h1>',
 +
$numLibros
 +
); </syntaxhighlight>
  
 
<br />
 
<br />
 +
 
=== Consulta chamando a funcións ===
 
=== Consulta chamando a funcións ===
  
−
* Dende Mysql pódense definir funcións, que son procedementos almacenados que devolven un valor dun tipo de datos.
+
* MySQL tamén permite definir funcións almacenadas.
  
−
: [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html Definicións de funcións dende Mysql].
+
* Unha función, a diferenza dun procedemento, devolve directamente un valor e pode empregarse dentro dunha expresión SQL.
  
 +
* Por exemplo:
  
−
* Vexamos un exemplo:
+
::<syntaxhighlight lang="sql">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE FUNCTION `EXEMPLO_FUNCION`() RETURNS int
+
CREATE FUNCTION CONSULTAR_NUM_LIBROS()
 +
RETURNS INT
 +
READS SQL DATA
 
BEGIN
 
BEGIN
−
    declare variable int;
 
  
−
SELECT col2
+
DECLARE num_libros INT;
−
INTO variable
+
 
−
FROM TABLA_1
+
SELECT COUNT(*)
−
        WHERE col1=1;
+
INTO num_libros
 +
FROM libros;
 +
 
 +
RETURN num_libros;
 +
 
 +
END$$
 +
 
 +
DELIMITER ; </syntaxhighlight>
 +
 
 +
==== Empregando PDO ====
  
−
return variable;
+
* Podemos chamar á función mediante un `SELECT`:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT CONSULTAR_NUM_LIBROS()'
 +
);
  
−
END
+
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
−
$$
+
 
−
</syntaxhighlight>  
+
* `fetchColumn()` resulta especialmente útil cando unha consulta devolve un único valor.
 +
 
 +
==== Funcións con parámetros ====
 +
 
 +
* Supoñamos unha función:
 +
 
 +
::<syntaxhighlight lang="sql">
 +
SELECT Suma(5, 7) </syntaxhighlight>
  
−
: Como vemos, esta función vai devolver un número de tipo integer.
+
* Desde PDO:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT Suma(:a, :b)'
 +
);
  
−
* Para chamar á función dende Mysql teremos que 'engadila' dentro dunha cláusula SELECT.
+
$stmt->bindValue(
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
':a',
−
define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
+
5,
−
</syntaxhighlight>
+
PDO::PARAM_INT
 +
);
  
−
: Como vemos, a tratamos coma dunha cláusula SELECT calquera, dándolle un nome (alias) ao resultado da execución da función.
+
$stmt->bindValue(
 +
':b',
 +
7,
 +
PDO::PARAM_INT
 +
);
  
 +
$stmt->execute();
  
−
* Agora soamente queda obter o resultado como fixemos [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Consulta no exemplo de consulta], anteriormente visto.
+
$resultado = $stmt->fetchColumn(); </syntaxhighlight>
  
 +
<br />
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="19" >
+
=== Paxinación de resultados ===
  
−
<?php
+
* En moitas ocasións unha consulta pode devolver unha cantidade elevada de rexistros.
  
−
define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
+
: Para evitar amosalos todos simultaneamente podemos dividir o resultado en páxinas.
  
−
$conex = new mysqli('SERVIDOR','USUARIO','PASSWORD','BD');
+
* En MySQL podemos empregar `LIMIT` e `OFFSET`.
−
$conex->set_charset('utf8');
 
  
−
if ($conex->connect_error){
+
* Por exemplo:
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
  
−
?>
+
::<syntaxhighlight lang="sql">
 +
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT 2 OFFSET 0; </syntaxhighlight>
  
 +
: Devolverá os dous primeiros rexistros.
  
−
        <?php
+
::<syntaxhighlight lang="sql">
−
            $result=$conex->query(FUNCION_EXEMPLO_MYSQL);
+
SELECT id, titulo
−
            if ($result || ($result->num_rows>0)){
+
FROM libros
−
                $row = $result->fetch_assoc();
+
ORDER BY titulo
−
                printf("<h1>Dato devolto:%d</h1>",$row['valor_devolto']);
+
LIMIT 2 OFFSET 2; </syntaxhighlight>
−
                $result->free();
 
−
            }
 
−
            else {
 
−
                echo "Fallou a chamada á función: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
  
−
       
+
: Devolverá os dous seguintes.
−
            $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
−
</syntaxhighlight>
 
  
−
:<u>Nota:</u> Fixarse como na liña 19 a referencia que utilizamos é o alias posto no DEFINE.
+
==== Pasos necesarios para realizar a paxinación ====
  
 +
* Necesitamos:
  
 +
:# Coñecer o número total de rexistros.
 +
:# Establecer cantos rexistros se amosarán por páxina.
 +
:# Coñecer a páxina actual.
 +
:# Calcular o número total de páxinas.
 +
:# Calcular o desprazamento (`OFFSET`).
 +
:# Realizar a consulta correspondente.
  
  
 
<br />
 
<br />
−
==== Caso Práctico: Consulta chamando a funcións ====
+
'''Número total de rexistros'''
  
−
* Imos facer un exemplo aplicado ao noso proxecto.
+
::<syntaxhighlight lang="php">
−
: Imos facer que o número de libros da nosa tenda sexa obtido utilizando unha función e non o procedemento almacenado con parámetro de saída da sección anterior.
+
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
  
−
* '''Creamos a función en Mysql'''
+
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
 
−
DELIMITER $$
+
 
 +
 
 +
<br />
 +
'''Número de rexistros por páxina'''
  
−
CREATE FUNCTION `CONSULTAR_NUM_LIBROS`() RETURNS int READS SQL DATA
+
::<syntaxhighlight lang="php">
−
BEGIN
+
define(
−
    declare num_libros int;
+
'CONST_NUM_LIBROS_PAXINA',
 +
10
 +
); </syntaxhighlight>
  
−
SELECT COUNT(*)
 
−
INTO num_libros
 
−
FROM libros;
 
  
−
return num_libros;
+
<br />
−
END$$
+
'''Obter e validar a páxina actual'''
−
</syntaxhighlight>  
 
  
 +
* O número de páxina chegará normalmente mediante `GET`.
  
 +
::<syntaxhighlight lang="php">
 +
$paxinaActual = filter_input(
 +
INPUT_GET,
 +
'paxina',
 +
FILTER_VALIDATE_INT
 +
);
  
−
* '''Aqruivo bd.inc.php'''
+
if (
−
: Definimos a constante que vai chamar á función.
+
$paxinaActual === false ||
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
$paxinaActual === null ||
−
define('FUNCION_CONSULTAR_NUM_LIBROS','select CONSULTAR_NUM_LIBROS() as num_libros');
+
$paxinaActual < 1
−
</syntaxhighlight>
+
) {
  
−
: Fixarse no alias da chamada á función.
+
$paxinaActual = 1;
  
 +
} </syntaxhighlight>
  
−
* '''Creamos unha páxina consultar_num_libros_con_funcionMYSQL.php'''
+
* `htmlspecialchars()` non serve para validar que un dato sexa numérico.
−
::<syntaxhighlight lang="java" line 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.inc.php');
+
: Neste caso empregamos `FILTER_VALIDATE_INT`.
  
−
try {
 
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
$erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
<br />
−
?>
+
'''Número total de páxinas'''
−
<!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>Operacións BD</title>
 
−
       
 
−
    </head>
 
  
−
    <body>
+
::<syntaxhighlight lang="php">
 +
$totalPaxinas = (int) ceil(
 +
$numLibros /
 +
CONST_NUM_LIBROS_PAXINA
 +
); </syntaxhighlight>
  
−
        <?php
+
* Podemos evitar que o usuario solicite unha páxina superior á existente:
−
        $result=$conex->query(FUNCION_CONSULTAR_NUM_LIBROS);
 
−
        if ($result && ($result->num_rows>0)){
 
−
            $row = $result->fetch_assoc();
 
−
            printf("<h1>N&uacute;mero de libros:%d</h1>",$row['num_libros']);
 
−
            $result->free();
 
−
        }
 
−
        else {
 
−
            echo "Fallou a chamada á función.: (" . $conex->errno . ") " . $conex->error;
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
−
<?php
 
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="php">
 +
if (
 +
$totalPaxinas > 0 &&
 +
$paxinaActual > $totalPaxinas
 +
) {
  
 +
$paxinaActual = $totalPaxinas;
  
 +
} </syntaxhighlight>
  
  
 
<br />
 
<br />
−
=== Paxinación de resultados ===
+
'''Calcular o OFFSET'''
  
−
* En moitas ocasións imos necesitar paxinar os resultados dunha consulta.
+
::<syntaxhighlight lang="php">
−
: Isto é necesario cando o resultado pode devolver unha cantidade moi grande de rexistros que obrigaría ao usuario a facer demasiado scroll.
+
$inicio = (
 +
$paxinaActual - 1
 +
) * CONST_NUM_LIBROS_PAXINA; </syntaxhighlight>
  
  
−
* A idea é moi simple. Utilizando a cláusula [https://dev.mysql.com/doc/refman/5.5/en/limit-optimization.html '''LIMIT''' da orde SQL Select].
 
−
: Dita cláusula pode levar dous parámetros separados por coma: LIMIT numero_fila,tamaño e vai ao final da orde SELECT.
 
  
−
: Por exemplo:
+
<br />
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
+
'''Consulta paxinada'''
−
SELECT id,titulo
 
−
FROM libros
 
−
ORDER by titulo
 
−
LIMIT 0,2
 
−
</syntaxhighlight>
 
  
−
:: Este exemplo amosaría os dous primeiros rexistros empezando polo primeiro do conxunto de resultados (rexistros 0 e 1)
+
* Como `LIMIT` e `OFFSET` necesitan valores numéricos, indicaremos explicitamente o tipo mediante `PDO::PARAM_INT`.
  
−
: Se modifico a orde e poño:
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
+
$stmt = $conex->prepare(
−
SELECT id,titulo
+
'SELECT id, titulo
 
FROM libros
 
FROM libros
−
ORDER by titulo
+
ORDER BY titulo
−
LIMIT 2,2
+
LIMIT :limite
−
</syntaxhighlight>
+
OFFSET :inicio'
 +
);
  
−
:: Devolverá os rexistros 2 e 3 do conxunto de resultados. Estamos paxinando os resultados de dous en dous !!!!
+
$stmt->bindValue(
 +
':limite',
 +
CONST_NUM_LIBROS_PAXINA,
 +
PDO::PARAM_INT
 +
);
  
 +
$stmt->bindValue(
 +
':inicio',
 +
$inicio,
 +
PDO::PARAM_INT
 +
);
  
−
* Polo tanto para paxinar os datos dunha consulta terei que:
+
$stmt->execute();
−
:* Establecer o número de rexistros por páxina. Isto será o que leve a cláusula LIMIT como segundo dato.
 
−
:* Gardar a páxina actual e mandar dito dato entre as diferentes peticións da páxina PHP (normalmente enlaces có número de páxina).
 
−
:* Para saber o número de páxinas terei que obter o número de rexistros en total, xa que o número de páxinas será igual ao número de rexistros dividido (parte enteira) entre o número de rexistros por páxina.
 
−
:: Para isto faremos uso da [http://php.net/manual/es/function.ceil.php función ceil] que devolve o número enteiro redondeado cara arriba.
 
−
:: Así, se teño 10 elementos e quero paxinar 3 elementos por páxina: ceil(10/3) = '''4''' que serán o número de páxinas.
 
−
 
 
  
 +
$libros = $stmt->fetchAll(); </syntaxhighlight>
  
  
 
<br />
 
<br />
−
==== Caso Práctico: Paxinación de resultados ====
+
'''Amosar os resultados'''
  
 +
::<syntaxhighlight lang="php">
 +
if ($libros) {
  
−
* Imos crear unha nova páxina de nome '''consultar_libros_con_paxinacion.php''' que partindo do código da páxina 'consultar_libros.php' feita previamente, amose os libros con paxinación.
+
echo "<table border='1'>";
  
−
: O faremos utilizando un procedemento almacenado (de nome CONSULTAR_LIBROS_PAXINACION), que vai levar dous parámetros, o número de páxina actual e o número de libros por páxina (tamén se podería facer directamente cun select dende PHP sen utilizar procedementos almacenados, pero xa vimos que é máis seguro).
+
foreach ($libros as $libro) {
  
 +
    echo '<tr>';
  
−
* Aplicado ao noso exemplo, imos engadir máis datos para poder facer unha paxinación.
+
    printf(
 +
        '<td>%s</td>',
 +
        htmlspecialchars(
 +
            $libro['titulo'],
 +
            ENT_QUOTES | ENT_SUBSTITUTE,
 +
            'UTF-8'
 +
        )
 +
    );
  
−
: '''Táboa autores'''
+
    echo '</tr>';
 +
}
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
echo '</table>';
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('ABBOTT', 'RACHEL');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('HJORTH', 'MICHAEL');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('LAURENS', 'STEPHANIE');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('ESTRINGANA', 'MORUENA');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('NIXON', 'RITA');
 
−
</syntaxhighlight>
 
  
 +
} else {
  
−
: '''Táboa libros'''
+
echo 'Non hai datos que amosar.';
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
} </syntaxhighlight>
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('COMO UNA EXTRAÑA');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('CRÍMENES DUPLICADOS');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('A SALVO CON SU CAPTOR');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('MI ERROR FUE NO SER YO MISMA. PARTE II');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('EL AMOR NO ES UN INVENTO DE LOS POETAS');
 
−
</syntaxhighlight>
 
  
  
−
: '''Táboa libro_autor'''
 
−
:: Teredes que ter en conta os identificadores autonuméricos xerados nos insert´s anteriores.
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('3', '2');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('4', '3');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('5', '4');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('6', '5');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('7', '6');
 
−
</syntaxhighlight>
 
  
 +
<br />
 +
'''Amosar os enlaces ás páxinas'''
  
−
: '''Táboa exemplares'''
+
::<syntaxhighlight lang="php">
−
:: Teredes que ter en conta os identificadores autonuméricos xerados nos insert´s da táboa libros.
+
echo '<div>';
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `prezo`, `estado_id`) VALUES ('3', '1', '9.99', '1');
 
−
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `prezo`, `estado_id`) VALUES ('3', '2', '9.99', '1');
 
−
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('4', '1', '', '29.99', '1');
 
−
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `prezo`, `estado_id`) VALUES ('5', '1', '23.35', '1');
 
−
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('6', '1', '', '34.76', '1');
 
−
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('7', '1', '', '22.54', '1');
 
−
</syntaxhighlight>
 
  
 +
for (
 +
$cont = 1;
 +
$cont <= $totalPaxinas;
 +
$cont++
 +
) {
  
−
* Creamos o procedemento almacenado en Mysql para que devolva os libros con paxinación.
+
if ($cont === $paxinaActual) {
−
: Poderíamos modificar o que xa tiñamos, comprobando se enviamos valores aos parámetros ou non (valor null) e facendo un select ou outro en función dos valores...
 
  
−
'''Procedemento almacenado CONSULTAR_LIBROS_PAXINACION en Mysql'''
+
    printf(
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
        '<strong>%d</strong> ',
−
DELIMITER $$
+
        $cont
 +
    );
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_LIBROS_PAXINACION`(IN p_num_rexistro int,IN p_tam_paxina INT)
+
} else {
−
BEGIN
 
−
SELECT id,titulo
 
−
FROM libros
 
−
ORDER BY titulo
 
−
LIMIT p_num_rexistro,p_tam_paxina;
 
  
−
END$$
+
    printf(
−
</syntaxhighlight>
+
        "<a href='?paxina=%d'>%d</a> ",
 +
        $cont,
 +
        $cont
 +
    );
 +
}
  
 +
}
  
 +
echo '</div>'; </syntaxhighlight>
  
−
* Creamos a constante que vai facer uso deste procedemento.
 
−
'''Arquivo: bd.inc.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_PAXINACION','CONSULTAR_LIBROS_PAXINACION(%d,%d)');
 
−
</syntaxhighlight>
 
  
−
:: Como vemos imos enviar dous parámetros.
 
  
 +
==== Exemplo completo de paxinación ====
  
  
  
−
* Partimos do código da páxina consultar_libros.php e <u>creamos unha nova páxina</u> de nome '''consultar_libros_con_paxinacion.php'''
+
'''Aquivo bd.php'''
−
: Nesta páxina voi definir nunha constante o número de libros por páxina. Se este dato fora compartido por outras páxinas, debería no arquivo bc.inc.php
+
::<syntaxhighlight lang="php">
 +
define(
 +
    'CONST_NUM_LIBROS_PAXINA',
 +
    10
 +
);
 +
::</syntaxhighlight>
  
  
−
: '''Arquivo: consultar_libros_con_paxinacion.php'''
+
'''Páxina php a cargar'''
 +
::<syntaxhighlight lang="php">
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="8,31-42,48-68,71-72" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
 
−
ini_set('display_errors', 1);
+
ini_set('display_errors', '1');
−
ini_set('display_startup_errors', 1);
 
 
error_reporting(E_ALL);
 
error_reporting(E_ALL);
  
−
require ('/var/www/confBD/bd.inc.php');
+
require '/var/www/confBD/bd.php';
−
define('CONST_NUM_LIBROS_PAXINA',2);   // Podería ir en bd.inc.php ou noutro arquivo inc.php se vai ser empregada por outras páxinas
 
  
  
 
try {
 
try {
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD);  // Non indicamos o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
−
    $conex->select_db(BD); // O parámetro indica o nome da base de datos
 
  
−
?>
+
    $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
 +
        ]
 +
    );
 +
 
 +
 
 +
    /*
 +
    * Número total de libros.
 +
    */
 +
    $stmt = $conex->query(
 +
        'SELECT COUNT(*)
 +
        FROM libros'
 +
    );
 +
 
 +
    $numLibros = (int) $stmt->fetchColumn();
 +
 
 +
 
 +
    /*
 +
    * Número total de páxinas.
 +
    */
 +
    $totalPaxinas = (int) ceil(
 +
        $numLibros /
 +
        CONST_NUM_LIBROS_PAXINA
 +
    );
 +
 
 +
 
 +
    /*
 +
    * Páxina solicitada polo usuario.
 +
    */
 +
    $paxinaActual = filter_input(
 +
        INPUT_GET,
 +
        'paxina',
 +
        FILTER_VALIDATE_INT
 +
    );
 +
 
 +
    if (
 +
        $paxinaActual === false ||
 +
        $paxinaActual === null ||
 +
        $paxinaActual < 1
 +
    ) {
 +
 
 +
        $paxinaActual = 1;
 +
    }
 +
 
 +
 
 +
    if (
 +
        $totalPaxinas > 0 &&
 +
        $paxinaActual > $totalPaxinas
 +
    ) {
 +
 
 +
        $paxinaActual = $totalPaxinas;
 +
    }
 +
 
 +
 
 +
    /*
 +
    * Primeiro rexistro da páxina.
 +
    */
 +
    $inicio = (
 +
        $paxinaActual - 1
 +
    ) * CONST_NUM_LIBROS_PAXINA;
 +
 
 +
 
 +
    /*
 +
    * Consulta dos libros da páxina actual.
 +
    */
 +
    $stmt = $conex->prepare(
 +
        'SELECT id, titulo
 +
        FROM libros
 +
        ORDER BY titulo
 +
        LIMIT :limite
 +
        OFFSET :inicio'
 +
    );
 +
 
 +
    $stmt->bindValue(
 +
        ':limite',
 +
        CONST_NUM_LIBROS_PAXINA,
 +
        PDO::PARAM_INT
 +
    );
 +
 
 +
    $stmt->bindValue(
 +
        ':inicio',
 +
        $inicio,
 +
        PDO::PARAM_INT
 +
    );
 +
 
 +
    $stmt->execute();
 +
 
 +
    $libros = $stmt->fetchAll();
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
} catch (PDOException $e) {
−
    "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>Operacións BD</title>
 
−
    </head>
 
  
−
     <body>
+
     error_log($e->getMessage());
−
        <?php
+
    die('Erro relacionado coa base de datos.');
−
                // OBTEMOS O NUMERO DE LIBROS
 
−
                // Este código ven dun caso práctico anterior que devolve o número de libros
 
−
                //  facendo uso dun procedemento almacenado e dun parámetro de saída.
 
−
                $numLibros=0;
 
−
                $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();
 
−
                    if (!($resultado = $conex->query("SELECT @numero_libros as _p_out"))) {
 
−
                        die ("Fallou a obtención: (" . $conex->errno . ") " . $conex->error);
 
−
                    }
 
−
                    else {
 
−
                        $fila = $resultado->fetch_assoc();
 
−
                        $numLibros = $fila['_p_out'];
 
−
                    }
 
−
                }
 
−
            ?>
 
  
−
           
+
} catch (Throwable $e) {
−
            <?php  // $numLibros ten o número de libros da nosa táboa
 
−
                // AMOSAMOS AS PAXINAS EN BASE AO NUMERO TOTAL E O NUMERO POR PAXINA
 
−
                $total_paxinas = ceil($numLibros/CONST_NUM_LIBROS_PAXINA);
 
−
               
 
−
                // Establecemos cal é a páxina actual. Se o usuario elixiu algunha virá en forma de URL $_GET
 
−
                if(!empty($_GET['paxina'])){
 
−
                    $paxina_actual=htmlentities($_GET['paxina']);
 
−
                }
 
−
                else{
 
−
                    $paxina_actual=1;  // A primeira vez o usuario non pulsou ningunha páxina e polo tanto a páxina inicial é a 1
 
−
                }
 
−
               
 
−
                // 'Imprimimos as páxinas...
 
−
                echo "<div>";
 
−
                for($cont=1;$cont<=$total_paxinas;$cont++){
 
−
                    if ($cont==$paxina_actual){   // A páxina actual non leva link
 
−
                        echo $cont . ' ';
 
−
                    }
 
−
                    else{
 
−
                        printf("<a href='%s?paxina=%d'>%d</a> ",htmlentities($_SERVER['PHP_SELF']),$cont,$cont);
 
−
                    }
 
−
                }
 
−
                echo "</div>";
 
−
            ?>
 
−
            <?php       
 
−
                $rexistro_actual=($paxina_actual-1)*CONST_NUM_LIBROS_PAXINA;  // Restamos 1 á páxina actual xa que a páxina 1 empeza no rexistro 0
 
−
                $proc=sprintf(PROCEDEMENTO_CONSULTAR_LIBROS_PAXINACION,$conex->real_escape_string($rexistro_actual),CONST_NUM_LIBROS_PAXINA);   
 
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();
 
−
                    if ($result->num_rows>0){
 
−
                        echo "<table border='1'>";
 
−
                        while($row = $result->fetch_assoc()) {
 
−
                            echo '<tr>';
 
−
                            printf("<td>%s</td>",$row["titulo"]);
 
−
                            echo '</tr>';
 
−
                        }
 
−
                        echo '</table>';
 
−
                        $result->free();  // Liberamos da memoria os recursos
 
−
                    }
 
−
                    else {
 
−
                        echo "Non hai datos que amosar!!!!";
 
−
                    }
 
−
                }
 
−
        ?>       
 
  
−
     </body>   
+
     die('Produciuse un erro na aplicación.');
−
</html>
 
  
−
<?php
 
−
}    // Pecha o try do principio da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
 
}
 
}
 +
 
?>
 
?>
−
</syntaxhighlight>
 
  
−
* Liña 8: Indicamos o número de libros a amosar en cada páxina.
+
<!DOCTYPE html>
−
* Liñas 31-42: Obtemos o número de libros que temos na base de datos. Estamos a aproveitar o [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Caso_pr.C3.A1ctico:_Consulta_chamando_a_procedementos_almacenados_con_par.C3.A1metros_de_sa.C3.ADda código dun exercicio anterior] no que amosábamos como traballar cun parámetro de saída, pero neste caso sería máis sinxelo crear un novo procedemento ou unha función, que devolvera o número de libros.
+
 
−
* Liñas 48-68: Amosamos as páxinas en base ao número de rexistros totais e o número de libros a amosar por páxina.
+
<html lang="gl">
−
* Liñas 71-72: Pasamos ao procedemento almacenado a partires de que número de libro e a cantidade de libros que vai devolver o select no procedemento almacenado.
+
 
 +
<head>
 +
    <meta charset="UTF-8">
 +
    <title>Paxinación de libros</title>
 +
</head>
 +
 
 +
<body>
 +
 
 +
<?php if ($libros): ?>
 +
 
 +
<table border="1">
 +
 
 +
    <?php foreach ($libros as $libro): ?>
  
 +
        <tr>
 +
            <td>
 +
                <?= htmlspecialchars(
 +
                    $libro['titulo'],
 +
                    ENT_QUOTES | ENT_SUBSTITUTE,
 +
                    'UTF-8'
 +
                ) ?>
 +
            </td>
 +
        </tr>
  
 +
    <?php endforeach; ?>
  
 +
</table>
  
 +
<?php else: ?>
  
 +
<p>Non hai libros que amosar.</p>
  
−
<br />
+
<?php endif; ?>
−
===== Optimización =====
 
  
−
* O exercicio anterior poderíamos optimizalo para non ter que repetir a chamada a consultar o número de rexistros totais.
+
<?php if ($totalPaxinas > 1): ?>
−
: Esta chamada a estamos a facer en cada consulta á páxina.
 
−
: Se non estamos nun entorno multi-usuario no que se poida dar o caso de que se estean a dar de alta novos rexistros de forma continua, podemos optimizar o código e facer que a chamada a consultar o número de rexistros totais se faga unha vez.
 
  
 +
<div>
  
 +
    <?php
 +
    for (
 +
        $cont = 1;
 +
        $cont <= $totalPaxinas;
 +
        $cont++
 +
    ):
 +
    ?>
  
−
* Deberemos gardar nalgunha variable o número de rexistros totais e que dita información se manteña entre chamadas cando cambiamos de número de páxina.
+
        <?php if ($cont === $paxinaActual): ?>
−
* <u>Posibles solucións</u>:
 
−
:* Utilizar coookies.
 
−
:* Utilizar variables de sesión.
 
−
:* Aproveitar o formulario da paxinación e enviar en forma de campo oculto o número de rexistros totais.
 
−
:* Enviar o dato na URL a maiores do número de páxina.
 
  
 +
            <strong>
 +
                <?= $cont ?>
 +
            </strong>
  
 +
        <?php else: ?>
  
−
* Posible solución enviando o número total de libros na URL:
+
            <a href="?paxina=<?= $cont ?>">
 +
                <?= $cont ?>
 +
            </a>
  
−
'''Arquivo consultar_libros_con_paxinacion.php'''
+
        <?php endif; ?>
−
::<syntaxhighlight lang="java" line enclose="div" highlight="9-11,47" >
 
−
.......................
 
  
 +
    <?php endfor; ?>
  
 +
</div>
  
−
            <?php
+
<?php endif; ?>
−
                // OBTEMOS O NUMERO DE LIBROS
 
−
                // Este código ven dun caso práctico anterior que devolve o número de libros
 
−
                //  facendo uso dun procedemento almacenado e dun parámetro de saída.
 
−
                if (!empty($_GET['numtotal'])){ // Ven o número de libros totais na URL
 
−
                    $numLibros = htmlentities($_GET['numtotal']);  
 
−
                }
 
−
                else{
 
−
                    $numLibros=0;
 
−
                    $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
 
−
                    if ($conex->multi_query($proc)){
 
−
                        $result = $conex->store_result();
 
−
                        if (!($resultado = $conex->query("SELECT @numero_libros as _p_out"))) {
 
−
                            die ("Fallou a obtención: (" . $conex->errno . ") " . $conex->error);
 
−
                        }
 
−
                        else {
 
−
                            $fila = $resultado->fetch_assoc();
 
−
                            $numLibros = $fila['_p_out'];
 
−
                        }
 
−
                    }
 
−
   
 
−
                }
 
−
            ?>
 
−
            <?php  // $numLibros ten o número de libros da nosa táboa
 
−
                // AMOSAMOS AS PAXINAS EN BASE AO NUMERO TOTAL E O NUMERO POR PAXINA
 
−
                $total_paxinas = ceil($numLibros/CONST_NUM_LIBROS_PAXINA);
 
−
               
 
−
                // Establecemos cal é a páxina actual. Se o usuario elixiu algunha virá en forma de URL $_GET
 
−
                if(!empty($_GET['paxina'])){
 
−
                    $paxina_actual=htmlentities($_GET['paxina']);
 
−
                }
 
−
                else{
 
−
                    $paxina_actual=1;  // A primeira vez o usuario non pulsou ningunha páxina e polo tanto a páxina inicial é a 1
 
−
                }
 
−
               
 
−
                // 'Imprimimos as páxinas...
 
−
                echo "<div>";
 
−
                for($cont=1;$cont<=$total_paxinas;$cont++){
 
−
                    if ($cont==$paxina_actual){  // A páxina actual non leva link
 
−
                        echo $cont . ' ';
 
−
                    }
 
−
                    else{
 
−
                        printf("<a href='%s?paxina=%d&numtotal=%d'>%d</a> ",htmlentities($_SERVER['PHP_SELF']),$cont,$numLibros,$cont);
 
−
                    }
 
−
                }
 
−
                echo "</div>";
 
−
            ?>
 
  
 +
</body>
 +
</html>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
==== Sobre a optimización da paxinación ====
 +
 +
* En xeral non debemos enviar pola URL o número total de rexistros e confiar posteriormente nese dato:
 +
 +
::<syntaxhighlight lang="text">
 +
?paxina=2&numtotal=50 </syntaxhighlight>
 +
 +
: Os datos enviados mediante `GET` poden ser modificados polo usuario.
 +
 +
* Gardar o número total de rexistros nunha variable de sesión tampouco garante que o dato siga sendo correcto, xa que outros usuarios poderían inserir ou eliminar rexistros.
 +
 +
* Como primeira solución realizaremos en cada petición:
 +
 +
::<syntaxhighlight lang="sql">
 +
SELECT COUNT(*)
 +
FROM libros </syntaxhighlight>
 +
 +
* Se nunha aplicación real esta operación chegase a ser un problema de rendemento poderían estudarse técnicas de caché ou outras estratexias de optimización.
 +
 +
<br />
 +
 +
=== Exercicio proposto ===
 +
 +
* Crea unha páxina chamada `buscar_libros.php` que permita buscar libros empregando os seguintes criterios:
 +
 +
:* '''Título''': unha caixa de texto que realizará unha busca mediante `LIKE`.
 +
:* '''Prezo''': un campo no que se validará que o valor introducido sexa numérico.
 +
:* '''Estado''': unha lista `<select>` cos estados dispoñibles.
 +
:* '''Autor''': unha lista `<select>` cos autores dispoñibles.
 +
:* '''Criterio de ordenación''': título, prezo ou autor.
 +
 +
* O usuario poderá combinar os diferentes criterios.
 +
 +
* Os resultados deberán poder amosarse paxinados.
 +
 +
* A solución deberá:
  
 +
:* Empregar PDO.
 +
:* Empregar sentenzas preparadas para todos os datos externos.
 +
:* Validar os valores recibidos mediante `GET` ou `POST`.
 +
:* Empregar `bindValue()` co tipo correspondente cando sexa necesario.
 +
:* Non concatenar directamente datos introducidos polo usuario dentro dunha sentenza SQL.
 +
:* Empregar `htmlspecialchars()` cando se amosen cadeas dentro do HTML.
 +
:* Capturar os erros PDO mediante `PDOException`.
 +
:* Rexistrar os detalles do erro mediante `error_log()`.
 +
:* Amosar ao usuario unha mensaxe xenérica en caso de erro.
 +
:* Capturar outros erros da aplicación mediante `Throwable`.
  
 
<br />
 
<br />
−
* Posible solución utilizando [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Sesi%C3%B3ns variables de sesión] (veremos o que son nas seguintes seccións):
 
  
−
'''Arquivo consultar_libros_con_paxinacion.php'''
+
== Resumo das funcións PDO empregadas ==
−
::<syntaxhighlight lang="java" line enclose="div" highlight="1-4,9,21,29" >
 
−
<?php
 
−
  session_start();  // Iría ao principio de todo
 
  
−
?>
+
::<syntaxhighlight lang="php">
−
.......................
+
! Método / constante
  
 +
| ! Uso                                                                                            |
 +
| ------------------------------------------------------------------------------------------------- |
 +
| `new PDO(...)`                                                                                    |
 +
| Abrir unha conexión coa base de datos.                                                            |
 +
| -                                                                                                |
 +
| `query()`                                                                                        |
 +
| Executar unha sentenza SQL sen parámetros externos e obter un resultado.                          |
 +
| -                                                                                                |
 +
| `prepare()`                                                                                      |
 +
| Preparar unha sentenza SQL parametrizada.                                                        |
 +
| -                                                                                                |
 +
| `execute()`                                                                                      |
 +
| Executar unha sentenza preparada.                                                                |
 +
| -                                                                                                |
 +
| `bindValue()`                                                                                    |
 +
| Asociar un valor e, opcionalmente, o seu tipo a un parámetro.                                    |
 +
| -                                                                                                |
 +
| `fetch()`                                                                                        |
 +
| Recuperar unha fila do resultado.                                                                |
 +
| -                                                                                                |
 +
| `fetchAll()`                                                                                      |
 +
| Recuperar todas as filas do resultado.                                                            |
 +
| -                                                                                                |
 +
| `fetchColumn()`                                                                                  |
 +
| Recuperar o valor dunha columna, especialmente útil para `COUNT(*)` ou funcións.                  |
 +
| -                                                                                                |
 +
| `closeCursor()`                                                                                  |
 +
| Liberar os resultados pendentes dunha sentenza, especialmente útil con procedementos almacenados. |
 +
| -                                                                                                |
 +
| `PDO::PARAM_INT`                                                                                  |
 +
| Indica un parámetro de tipo enteiro.                                                              |
 +
| -                                                                                                |
 +
| `PDO::PARAM_STR`                                                                                  |
 +
| Indica un parámetro de tipo cadea.                                                                |
 +
| -                                                                                                |
 +
| `PDO::PARAM_BOOL`                                                                                |
 +
| Indica un parámetro booleano.                                                                    |
 +
| -                                                                                                |
 +
| `PDO::PARAM_NULL`                                                                                |
 +
| Indica un valor nulo.                                                                            |
 +
| -                                                                                                |
 +
| `PDO::FETCH_ASSOC`                                                                                |
 +
| Recupera as filas como arrays asociativos.                                                        |
 +
| }                                                                                                |
 +
::</syntaxhighlight>
  
−
            <?php
+
== Regras que empregaremos nos exames ==
−
                if (empty($_SESSION['numRexistrosTotais']) {
 
−
                  // OBTEMOS O NUMERO DE LIBROS
 
−
                  $numLibros=0;
 
−
                  $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
 
−
                  if ($conex->multi_query($proc)){
 
−
                      $result = $conex->store_result();
 
−
                      if (!($resultado = $conex->query("SELECT @numero_libros as _p_out"))) {
 
−
                          die ("Fallou a obtención: (" . $conex->errno . ") " . $conex->error);
 
−
                      }
 
−
                      else {
 
−
                          $fila = $resultado->fetch_assoc();
 
−
                          $numLibros = $fila['_p_out'];
 
−
                          $_SESSION['numRexistrosTotais'] = $numLibros;
 
−
                      }
 
−
                  }
 
−
                }
 
−
                else {
 
−
                    $numLibros = $_SESSION['numRexistrosTotais'];
 
−
                }
 
−
            ?>
 
−
            ...................
 
  
−
</syntaxhighlight>
+
# As operacións contra a base de datos realizaranse mediante '''PDO'''.
  
 +
# Para consultas sen parámetros externos poderá empregarse `query()`.
  
 +
# Se hai datos procedentes do usuario ou outros valores externos empregarase `prepare()` + `execute()`.
  
 +
# Non se concatenarán directamente datos externos nas sentenzas SQL.
  
 +
# Os datos recibidos mediante formularios ou URL deberán validarse.
  
 +
# Cando o tipo sexa relevante empregarase `bindValue()` indicando `PDO::PARAM_INT`, `PDO::PARAM_STR`, etc.
  
 +
# Non se empregará `rowCount()` para coñecer o número de filas devoltas por un `SELECT`.
  
−
<br />
+
# Se necesitamos contar filas empregaremos `COUNT(*)`.
−
=== Exercicio proposto ===
 
  
−
* Crea unha páxina de nome '''buscar_libros.php''' no que se amose nun formulario as seguintes opcións de busca de libros:
+
# Para amosar cadeas dentro de HTML empregarase `htmlspecialchars()`.
−
:* Busca por título: Será unha caixa de texto limitada a 50 caracteres que fará uso de like (busca por patróns)
 
−
:* Busca por prezo: Estará implementada en forma de caixa limitada a 6 caracteres (tres números, unha coma e dous decimais). Deberedes verificar que o formato sexa correcto.
 
−
:* Busca por estado: Estará implementada en forma de lista (<select> en html con size) (opcionalmente podedes permitir que o usuario poida escoller varios estados).
 
−
:* Busca por autor: Estará implementada en forma de combobox (<select> en html)
 
  
−
:* Criterios de ordenación: Por título, prezo e autor (implementalo con radiobuttons).
+
# Os erros PDO capturaranse mediante `PDOException`.
  
−
* O usuario vai poder escoller calquera das opcións combinándoas como prefira.
+
# Os detalles técnicos dos erros rexistraranse mediante `error_log()` e non se amosarán ao usuario final.
−
* Os resultados van poder amosarse paxinados. O número de libros por páxina estará indicado nunha caixa de texto co seu propio formulario.
 
  
−
* O resultado vai ser unha lista de libros, os seus autores e os exemplares, ordenados por título
+
# Os demais erros ou excepcións poderán capturarse mediante `Throwable`.
−
* Implementa a solución utilizando consultas.
 
  
−
* Quen queira tamén pode intentalo facer con procedementos almacenados pero a consulta teredes que facela dinamicamente dentro do procedementos.
+
# Os exemplos realizaranse tomando como referencia '''PHP 8.5'''.
−
: Podedes consultar como facelo [https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html neste enlace].
 
  
 +
<br />
  
  
Línea 2662: Línea 3575:
  
  
−
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2023).
+
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2025).

Revisión actual del 18:12 8 oct 2026

Sumario

Introducción


IMPORTANTE: Nos exames, exercicios e prácticas avaliables empregarase PDO para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario. Os exemplos con mysqli poden manterse nos apuntes a nivel informativo, pero os exemplos de referencia serán os realizados con PDO.



  • O proceso para realizar unha consulta é o seguinte:


  • Necesitamos ter unha consulta SQL para recuperar datos.
Indicar que o SQL (Structure Query Language) é a linguaxe utilizada para obter datos dunha base de datos relacional.
Indicar que estos contidos xa deben estar aprendidos do módulo de Base de datos e saber facer joins entre táboas, subconsultas, agrupacións,...


  • CONSIDERACIÓNS:
  • Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos)
  • Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como vimos nun punto anterior desta sección.
  • Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas.
  • En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes.
  • Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...).
  • Lembrar que tamén podedes activar o debugger como está indicado nesta sección da wiki.



Realización dunha consulta SELECT

  • Paso 2: Chamar ao procedemento query enviando como parámetro a consulta a realizar e a conexión aberta previamente.
Consulta SQL: SELECT col1, col2 FROM TABOA_1 ORDER BY col
  • Se a consulta SQL non leva datos externos podemos empregar directamente o método query():
$stmt = $conex->query(
'SELECT col1, col2
FROM TABOA_1
ORDER BY col1'
);
query() devolve un obxecto da clase PDOStatement.


  • Se a consulta leva datos procedentes do usuario ou valores que queremos enviar mediante parámetros empregaremos:
prepare()
execute()
Por exemplo:
$stmt = $conex->prepare(
'SELECT col1, col2
FROM TABOA_1
WHERE col1 = :valor'
);
$stmt->execute([
'valor' => 5
]);




  • Version procedimental
Funcións para realizar consultas a base de datos e funcións relacionadas:
  • mysqli::query: Realiza a consulta
  • mysqli_result: En caso de realizar unha consulta de tipo 'select' este obxecto representa o conxunto de resultados obtidos.
  • int $mysqli_result->num_rows: Devolve o número de filas devoltas en caso de realizar unha consulta.
  • Para recuperar os datos podemos empregar:
  • array mysqli_result::fetch_assoc(void): Devolve nun array asociativo unha das filas do resultado da consulta e 'pasa' á fila seguinte. Cando remate e xa non teña máis filas devolve NULL. A clave do array asociativo é cada un dos nomes das columnas que conforman a consulta.
  • mysqli_result::fetch_object(void): Devolve nun array de obxectos todas as filas do resultado da consulta e 'pasa' á fila seguinte. Cando remate e xa non teña máis filas devolve NULL. Cada obxecto terá como propiedades cada unha das columnas que conforman a consulta (distingue maiúsculas de minúsculas).


NOTA: En todos os exercicios e exemplos vou facer uso de fetch_assoc para recuperar os datos, pero quen queira pode empregar fech_object. O cambio é case inmediato, o único será o acceso as propiedades que se fai có operador: ->


  • Versión procedimental:
$result=mysqli_query($conex, 'SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
Nota: $conex é a conexión aberta feita no paso anterior.
  • Versión orientada a obxectos e PDO:
$result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
  • $result vai ter o conxunto de filas que devolva o resultado. Podemos velo como unha táboa formada polas filas do resultado e polas columnas indicadas no select.



Comprobar se unha consulta SELECT devolveu datos

  • Non debemos empregar rowCount() para comprobar o número de filas devoltas por un SELECT.
rowCount() está pensado principalmente para coñecer o número de filas afectadas por operacións INSERT, UPDATE ou DELETE.
Nun SELECT o seu comportamento depende do controlador da base de datos e non está garantido.


  • Unha forma sinxela de comprobar se hai datos consiste en realizar un primeiro fetch():
$stmt = $conex->query(
'SELECT col1, col2
FROM TABOA_1
ORDER BY col1'
);
$fila = $stmt->fetch();
if ($fila !== false) {
do {

    echo htmlspecialchars(
        $fila['col1'],
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );

    echo ' - ';

    echo htmlspecialchars(
        $fila['col2'],
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );

    echo '<br>';

} while ($fila = $stmt->fetch());
} else {
echo 'Non hai datos que amosar';
}
  • Se sabemos que o número de filas vai ser pequeno tamén podemos recuperar todas as filas mediante fetchAll():
$stmt = $conex->query(
'SELECT col1, col2
FROM TABOA_1
ORDER BY col1'
);
$filas = $stmt->fetchAll();
if ($filas) {
foreach ($filas as $fila) {

    echo htmlspecialchars(
        $fila['col1'],
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );

    echo ' - ';

    echo htmlspecialchars(
        $fila['col2'],
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );

    echo '<br>';
}
} else {
echo 'Non hai datos que amosar';
}


  • fetchColum permite recuperar da primeira fila a columna indicada. Se non poñemos ningunha columna recuperar a primeira.
Por exemplo, se realmente necesitamos coñecer o número de rexistros, realizaremos unha consulta específica empregando COUNT(*) e como sabemos que vai devolver unha fila cunha columna podemos empregar fetchColumn:
$stmt = $conex->query(
'SELECT COUNT(*)
FROM libros'
);
$numLibros = $stmt->fetchColumn();




  • Versión orientada a obxectos:


  • Se estamos a empregar unha versión PHP superior ou igual á 8.1, os erros deben ser capturados con try-catch:
  • Versión orientada a obxectos:
<?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);

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

?>

<!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>Operacións BD</title>
    </head>

    <body>
    <?php        
        
        $result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
        if ($result && $result->num_rows>0){
                // Aquí procesamos os datos
        }
        else {
            echo "Non hai datos que amosar!!!!";
        }
        
    ?>        
    </body>    
</html>

<?php
}    // Pecha o try do principio da páxina
catch (mysqli_sql_exception $e) {
    die("Erro mysql: " . $e->getMessage());
}
catch (Expcetion $e) {
    die("Erro xeral: " . $e->getMessage());
}

?>



  • Como vemos, en caso de erro comprobamos se o xestor devolveu un código de erro, indicativo de que a sentenza SQL está mal feita. A información do código de erro non debe amosarse a un usuario final. Normalmente, se pon o echo de 'Non hai datos que amosar' e se estamos a desenvolver a aplicación e non devolve datos, faríamos un echo temporal do código de erro do xestor ou un echo da sentenza SQL, podendo executala no xestor Mysql para ver o erro.




Recuperación de filas

Paso 4: Agora necesitaremos percorrer dende o primeiro rexistro ata o último amosando o resultado.

  • O método fetch() recupera unha única fila do conxunto de resultados e avanza á seguinte.
while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo htmlspecialchars(
    $fila['titulo'],
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);
}
Fixarse que neste exemplo no facemos un if inicial có primeiro fetch para saber si hai datos ou non. Dependerá de vos como programadores determinar se é necesario para informar ao usuario o facer algún tipo de proceso nese caso.


  • Se na conexión establecemos:
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
non será necesario indicar PDO::FETCH_ASSOC en cada chamada:
while ($fila = $stmt->fetch()) {
echo $fila['titulo'];
}
PDO::FETCH_ASSOC o que fai é que Php soamente cre un array asociativo cós resultados. Se non o poñemos, devolve dous arrays, un no que se accede aos datos polo índice ($fila[0]) e outro asociativo por nome de columna ($fila['titulo']).


  • Non debemos chamar dúas veces seguidas a fetch() para procesar o mesmo conxunto de resultados.
Por exemplo, isto é incorrecto:
while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // ...
}
}
O primeiro fetch() xa consume unha fila e o segundo bucle comezaría na seguinte.
A forma correcta sería:
while ($row = $stmt->fetch()) {
// Procesamos a fila
}




  • Versión procedimental
Debemos entender que, inicialmente, despois de facer a consulta, temos un punteiro que está 'apuntando' xusto antes do primeiro rexistro de resultados. O que fai a función anterior é mover ese punteiro ao seguinte rexistro (a primeira vez apuntará ao primeiro do conxunto de resultados) e devolve nun array a fila á que está 'apuntando'.
Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false.


Isto o faremos cun bucle, chamando á función mysqli_result::fetch_assoc(void).
  • Versión procedimental:
if ($result && (mysqli_num_rows($resullt)>0)){
  // Veñen datos
   while($row = mysqli_fetch_assoc($result)) {
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
   }
}
else {
  echo 'Non hai datos ou a consulta está mal feita';
}


  • Versión orientada a obxectos:
if ($result && ($result->num_rows>0)){
  // Veñen datos
   while($row = $result->fetch_assoc()) {
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
   }
}
else {
  echo 'Non hai datos ou a consulta está mal feita';
}


  • Versión orientada a obxectos con fetch_object:
if ($result && ($result->num_rows>0)){
  // Veñen datos
   while($row = $result->fetch_object()) {
    printf("%s-%s",htmlentities($row->col1),htmlentities($row->col2));
   }
}
else {
  echo 'Non hai datos ou a consulta está mal feita';
}

Saída de datos en HTML

Para amosar datos dentro dunha páxina HTML empregaremos normalmente htmlspecialchars():

echo htmlspecialchars(
$fila['titulo'],
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);

Non debemos empregar urlencode() para codificar texto que simplemente se vai amosar ao usuario.

Por exemplo, para crear un enlace a un autor:
printf(
"<a href='detalle_autor.php?id=%d'>%s, %s</a>",
(int) $row['id'],
htmlspecialchars(
$row['apelidos'],
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
),
htmlspecialchars(
$row['nome'],
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
)
);
urlencode() ou rawurlencode() utilizaranse cando sexa necesario codificar datos que forman parte dunha URL.

Consultas con parámetros numéricos

Podemos empregar parámetros con nome:

define(
'CONSULTA_CON_PARAM_NUMERICO',
'SELECT col1, col2
FROM TABOA_1
WHERE col1 = :valor'
);

Preparamos e executamos a consulta:

$stmt = $conex->prepare(
CONSULTA_CON_PARAM_NUMERICO
);
$stmt->execute([
'valor' => 5
]);
IMPORTANTE: O nome do parámetro debe coincidir co definido na consulta.
Se na consulta temos:
WHERE col1 = :valor
debemos empregar:
'valor' => 5
e non outro nome como valor1.

Tipo dos parámetros

Cando enviamos directamente un array ao método execute():

$stmt->execute([
'valor' => 5
]);
PDO trata os valores do array como PDO::PARAM_STR.
O xestor de base de datos pode realizar posteriormente conversións de tipo segundo o contexto da consulta.

Na maioría das consultas sinxelas isto non supón un problema. Se queremos indicar explicitamente o tipo do parámetro podemos empregar bindValue():

$stmt = $conex->prepare(
'SELECT col1, col2
FROM TABOA_1
WHERE col1 = :valor'
);
$stmt->bindValue(
':valor',
5,
PDO::PARAM_INT
);
$stmt->execute();

Algúns tipos habituais son:

PDO::PARAM_INT
PDO::PARAM_STR
PDO::PARAM_BOOL
PDO::PARAM_NULL


  • É especialmente recomendable indicar explicitamente o tipo cando este sexa relevante para a consulta.
Por exemplo, nos parámetros empregados en LIMIT ou OFFSET:
$stmt = $conex->prepare(
'SELECT id, titulo
FROM libros
ORDER BY titulo
LIMIT :inicio,:cantidade'
);
$stmt->bindValue(
':inicio',
$inicio,
PDO::PARAM_INT
);
$stmt->bindValue(
':cantidade',
$cantidade,
PDO::PARAM_INT
);
$stmt->execute();

Parámetros de tipo cadea

Para enviar unha cadea empregaremos tamén unha sentenza preparada:

$stmt = $conex->prepare(
'SELECT id, apelidos, nome
FROM autores
WHERE apelidos = :apelidos'
);
$stmt->execute([
'apelidos' => $apelidos
]);


  • Se queremos facer unha busca mediante LIKE, o marcador debe representar o valor completo:
$stmt = $conex->prepare(
'SELECT id, apelidos, nome
FROM autores
WHERE apelidos LIKE :apelidos'
);
$stmt->execute([
'apelidos' => '%' . $apelidos . '%'
]);


  • Non debemos escribir:
WHERE apelidos LIKE '%:apelidos%'
xa que o marcador debe ocupar o lugar dun valor completo (PHP/PDO interpretará :apelidos como texto literal dentro das comiñas e non o substituirá pola variable).

Validación dos parámetros recibidos

As sentenzas preparadas evitan que os valores enviados como parámetros sexan interpretados como parte da instrución SQL, pero non substitúen a validación dos datos. Por exemplo, se esperamos recibir un identificador numérico desde un formulario:

$libroId = filter_input(
INPUT_POST,
'lstLibros',
FILTER_VALIDATE_INT
);
if (
$libroId !== false &&
$libroId !== null &&
$libroId !== -1
) {
$stmt = $conex->prepare(
    CONSULTAR_EXEMPLARES_POR_LIBRO
);

$stmt->bindValue(
    ':libro_id',
    $libroId,
    PDO::PARAM_INT
);

$stmt->execute();

// Procesamos os resultados
}

Caso práctico: consultar exemplares dun libro

Arquivo bd.php

define('CARTAFOL_IMAXES_WEB', 'http://localhost/imaxes/');

define(
'CONSULTAR_EXEMPLARES_POR_LIBRO',
'SELECT libro_id,
id_exemplar,
imaxe,
prezo,
estado_id
FROM exemplares
WHERE libro_id = :libro_id
ORDER BY estado_id'
);



  • Código PHP
<html>

<head>Páxina de exemplo
</head>

<body>

    <?php
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    error_reporting(E_ALL);

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

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

        $opciones = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false
        ];

        $conex = new PDO(
            $dsn,
            USUARIO,
            PASSWORD,
            $opciones
        );

        $libroId = filter_input(INPUT_POST,'lstLibros',FILTER_VALIDATE_INT);  // Como non temos feito a páxina de formulario na seguinte liña dou un valor específico...
        $libroId = 1;   // Este dato virá dunha caixa de texto dunha páxina anterior que terá como action do formulario esta páxina (pode facerse todo nunha mesma páxina)

        if (
            $libroId !== false &&
            $libroId !== null &&
            $libroId !== -1
        ) {
            $stmt = $conex->prepare(
                CONSULTAR_EXEMPLARES_POR_LIBRO
            );

            $stmt->bindValue(
                ':libro_id',
                $libroId,
                PDO::PARAM_INT
            );

            $stmt->execute();

            $fila = $stmt->fetch();

            if ($fila !== false) {

                echo "<table border='1'>";
                echo '<tr>';
                echo '<th>Num. exemplar</th>';
                echo '<th>Imaxe</th>';
                echo '<th>Prezo</th>';
                echo '<th>Estado</th>';
                echo '</tr>';

                do {

                    echo '<tr>';

                    printf(
                        '<td>%d</td>',
                        (int) $fila['id_exemplar']
                    );

                    printf(
                        "<td><img alt='%s' src='%s' width='42' height='42'></td>",
                        htmlspecialchars(
                            $fila['imaxe'] ?? '',
                            ENT_QUOTES | ENT_SUBSTITUTE,
                            'UTF-8'
                        ),
                        htmlspecialchars(
                            CARTAFOL_IMAXES_WEB . ($fila['imaxe'] ?? ''),
                            ENT_QUOTES | ENT_SUBSTITUTE,
                            'UTF-8'
                        )
                    );

                    printf(
                        '<td>%.2f</td>',
                        (float) ($fila['prezo'] ?? 0)
                    );

                    printf(
                        '<td>%d</td>',
                        (int) $fila['estado_id']
                    );

                    echo '</tr>';
                } while ($fila = $stmt->fetch());

                echo '</table>';
            } else {

                echo 'Non hai datos que amosar';
            }
        }
    } catch (PDOException $e) {
        die("Error PDO: " . $e->getMessage());
    } catch (Throwable $e) {
        die("Error general: " . $e->getMessage());
    }

    $conex = null;   // Poderíamos non poñer nada
    ?>
</body>

</html>

Procedementos almacenados empregando PDO

Os procedementos almacenados execútanse mediante a sentenza SQL CALL.

Por exemplo:
$stmt = $conex->prepare(
'CALL CONSULTAR_LIBROS()'
);
$stmt->execute();

Se o procedemento devolve un conxunto de resultados podemos percorrelo da forma habitual:

while ($fila = $stmt->fetch()) {
echo htmlspecialchars(
    $fila['titulo'],
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);
}

Procedementos con varios conxuntos de resultados

Un procedemento almacenado pode devolver máis dun conxunto de resultados. Para pasar ao seguinte conxunto empregaremos:

nextRowset()
Por exemplo:
$stmt = $conex->prepare(
'CALL PROCEDEMENTO_CON_VARIOS_SELECT()'
);
$stmt->execute();
do {
while ($fila = $stmt->fetch()) {

    // Procesar as filas deste conxunto
}
} while ($stmt->nextRowset());


+ Antes de executar outra consulta na mesma conexión debemos asegurarnos de que non queden resultados pendentes.

Podemos liberar os resultados restantes mediante:
$stmt->closeCursor();
Isto é especialmente importante despois de chamadas a procedementos almacenados en MySQL.


Por exemplo:
<?php
// 1. Executamos o procedemento almacenado que devolve varios SELECTs
$stmt = $conex->prepare('CALL PROCEDEMENTO_CON_VARIOS_SELECT()');
$stmt->execute();

// 2. Procesamos só o primeiro conxunto de resultados (por exemplo, os primeiros 3 datos)
if ($fila = $stmt->fetch()) {
    echo "Primeiro rexistro: " . $fila['nome'] . "<br>";
}

// 3. Pechamos o cursor para liberar a conexión
// Isto descarta calquera outra fila ou segundo resultset pendente
$stmt->closeCursor();

// 4. Agora xa podemos executar outra consulta na mesma conexión sen erros
$outraConsulta = $conex->query('SELECT * FROM outra_taboa');
$datos = $outraConsulta->fetchAll();
?>

Procedementos con parámetros de entrada

Os parámetros de entrada enviaranse empregando marcadores:

$stmt = $conex->prepare(
'CALL PROCEDEMENTO_CON_PARAMETROS(
:dato1,:dato2

)'
);
$stmt->bindValue(
':dato1',
$valorNum,
PDO::PARAM_INT
);
$stmt->bindValue(
':dato2',
$valorTexto,
PDO::PARAM_STR
);
$stmt->execute();
Desta forma non necesitamos construír a chamada mediante sprintf() nin concatenar os valores dentro da sentenza SQL.

Procedementos con parámetros de saída en MySQL

PDO define mecanismos xenéricos para parámetros de saída, pero o controlador PDO_MYSQL non xestiona correctamente PDO::PARAM_INPUT_OUTPUT. Por este motivo, con MySQL podemos empregar unha variable de usuario de MySQL para recuperar un parámetro OUT. Supoñamos este procedemento:

DELIMITER $$
CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
OUT p_num_libros INT
)
BEGIN
SELECT COUNT(*)
INTO p_num_libros
FROM libros;
END$$
DELIMITER ;

Desde PHP podemos facer:

$stmt = $conex->prepare(
'CALL CONSULTAR_NUM_LIBROS(
@numero_libros
)'
);
$stmt->execute();
/*
Liberamos todos os resultados da chamada
antes de executar outra sentenza SQL.
*/
$stmt->closeCursor();
$stmt = $conex->query(
'SELECT @numero_libros'
);
$numLibros = $stmt->fetchColumn();
printf(
'<h1>Número de libros: %d</h1>',
(int) $numLibros
);
É importante chamar a closeCursor() antes do segundo SELECT, xa que MySQL pode manter conxuntos de resultados pendentes da chamada ao procedemento.

Funcións almacenadas de MySQL

As funcións de MySQL poden empregarse dentro dunha instrución SELECT. Se a función non leva parámetros podemos facer:

$stmt = $conex->query(
'SELECT CONSULTAR_NUM_LIBROS()'
);
$numLibros = $stmt->fetchColumn();

Se leva parámetros empregaremos unha sentenza preparada:

$stmt = $conex->prepare(
'SELECT Suma(:dato1,:dato2 )'
);
$stmt->bindValue(
':dato1',
5,
PDO::PARAM_INT
);
$stmt->bindValue(
'':dato2,
7,
PDO::PARAM_INT
);
$stmt->execute();
$resultado = $stmt->fetchColumn();
fetchColumn() resulta especialmente cómodo cando a consulta devolve un único valor.

Paxinación de resultados empregando PDO

Para paxinar resultados empregaremos:

  • COUNT(*) para coñecer o número total de rexistros.
  • LIMIT para indicar a cantidade de rexistros que queremos recuperar.
  • OFFSET, ou o primeiro parámetro de LIMIT, para indicar desde que rexistro queremos comezar.

Por exemplo:

SELECT id, titulo
FROM libros
ORDER BY titulo
LIMIT 10 OFFSET 20
Esta consulta devolvería ata 10 rexistros empezando desde o rexistro situado na posición 20.



Número total de rexistros

$stmt = $conex->query(
'SELECT COUNT(*)
FROM libros'
);
$numLibros = (int) $stmt->fetchColumn();



Páxina actual O número de páxina recibido mediante GET é un dato externo e debe validarse.

$paxinaActual = filter_input(
INPUT_GET,
'paxina',
FILTER_VALIDATE_INT
);
if (
$paxinaActual === false ||
$paxinaActual === null ||
$paxinaActual < 1
) {
$paxinaActual = 1;
}
htmlspecialchars() non serve para validar que un dato sexa un número. Empregarase posteriormente cando teñamos que inserir unha cadea dentro de HTML.



Cálculo do número de páxinas

define(
'CONST_NUM_LIBROS_PAXINA',
10
);
$totalPaxinas = (int) ceil(
$numLibros / CONST_NUM_LIBROS_PAXINA
);

Podemos evitar que se solicite unha páxina superior á existente:

if (
$totalPaxinas > 0 &&
$paxinaActual > $totalPaxinas
) {
$paxinaActual = $totalPaxinas;
}



Calcular o desprazamento

$inicio = (
$paxinaActual - 1
) * CONST_NUM_LIBROS_PAXINA;
Por exemplo, se estamos:
  • Na páxina 1 => inicio = 0
  • Na páxina 2 => inicio = 10
  • Na páxina 3 => inicio = 20



Consulta paxinada Neste caso indicaremos explicitamente que os parámetros son enteiros:

$stmt = $conex->prepare(
'SELECT id, titulo
FROM libros
ORDER BY titulo
LIMIT :limite
OFFSET :desprazamento'
);
$stmt->bindValue(
':limite',
CONST_NUM_LIBROS_PAXINA,
PDO::PARAM_INT
);
$stmt->bindValue(
':desprazamento',
$inicio,
PDO::PARAM_INT
);
$stmt->execute();
$libros = $stmt->fetchAll();


  • Despois podemos amosar os libros:
if ($libros) {
echo "<table border='1'>";

foreach ($libros as $libro) {

    echo '<tr>';

    printf(
        '<td>%s</td>',
        htmlspecialchars(
            $libro['titulo'],
            ENT_QUOTES | ENT_SUBSTITUTE,
            'UTF-8'
        )
    );

    echo '</tr>';
}

echo '</table>';
} else {
echo 'Non hai datos que amosar';
}




Enlaces da paxinación

echo '<div>';
for (
$cont = 1;
$cont <= $totalPaxinas;
$cont++
) {
if ($cont === $paxinaActual) {

    printf(
        '<strong>%d</strong> ',
        $cont
    );

} else {

    printf(
        "<a href='?paxina=%d'>%d</a> ",
        $cont,
        $cont
    );
}
}
echo '</div>';



Sobre a optimización do número total de rexistros En xeral non deberiamos enviar o número total de rexistros na URL confiando despois nese valor:

?paxina=2&numtotal=50
Os datos enviados mediante GET poden ser modificados polo usuario.

Tampouco debemos asumir que gardar o número total nunha variable de sesión é sempre unha optimización correcta, xa que a base de datos pode cambiar e o valor almacenado quedar desactualizado. Como primeira solución, realizaremos en cada petición:

SELECT COUNT(*)
FROM libros
Se nunha aplicación real esta operación chegase a ser un problema de rendemento, poderían estudarse posteriormente técnicas de caché ou outras estratexias de optimización.

Xestión de excepcións

  • Nos exemplos completos empregaremos o seguinte esquema:
try {
    $conex = new PDO($dsn, $usuario, $contrasinal, [
        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 (monolog, etc.)
    error_log($e->getMessage());
    
    die('Erro relacionado coa base de datos.');
} catch (Throwable $e) {
    die('Produciuse un erro na aplicación.');
}

// Non fai falta "finally {$conex = null;}":
// Se o script continúa, destruirasie cando cumpra o seu ciclo;
// se entra nos catch, o die() xa finaliza o proceso e libera a memoria.
PDOException permite tratar especificamente os erros de PDO.
Throwable permite capturar outros erros ou excepcións producidos durante a execución.
Os bloques catch deben colocarse desde o máis específico ao máis xeral.



Exercicios prácticos

  • Partimos de que xa temos creada a base de datos nunha sección anterior desta wiki e gardados os datos necesarios para realizar a conexión no arquivo `bd.php`.
  • IMPORTANTE: Nos exames, exercicios e prácticas avaliables empregaremos PDO para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario.
  • Os exemplos con `mysqli` poden consultarse como información adicional, pero os exemplos de referencia dos apuntes serán os realizados mediante PDO.
  • Nos exemplos completos empregaremos o seguinte esquema:
No arquivo bd.php que se atopa fora da ruta /var/www/html gardaremos os datos da conexión e as consultar a 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 van acceder á base de datos terán este esquema:

try {

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

try {

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

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

}
  • `PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC` fai que, por defecto, as filas recuperadas mediante `fetch()` ou `fetchAll()` sexan arrays asociativos.
  • `PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION` fai que os erros producidos por PDO xeren unha excepción de tipo `PDOException`.
  • `PDOException` permítenos tratar especificamente os erros relacionados con PDO.
  • `Throwable` permite capturar outros erros ou excepcións producidos durante a execución da aplicación.
  • Os bloques `catch` deben ordenarse desde o máis específico ao máis xeral.
  • Nunha aplicación en produción non debemos amosar ao usuario a mensaxe devolta por `$e->getMessage()`. Podemos rexistrala mediante `error_log()` ou outro sistema de logs.


Caso Práctico 1

  • Imos crear unha consulta que devolva todos os libros da táboa `LIBROS` e os amose dentro dunha táboa HTML.
  • Para non ter as consultas SQL no medio do código das diferentes páxinas, definirémolas no arquivo `bd.php`.

Arquivo: bd.php

define(
'CONSULTAR_LIBROS',
'SELECT id, titulo
FROM libros
ORDER BY titulo'
);
  • Como norma para os nomes das constantes que conteñan sentenzas SQL:
  • Comezarán cun verbo que poderá ser `CONSULTAR`, `INSERIR`, `MODIFICAR` ou `ELIMINAR`.
  • A continuación empregarase un guión baixo.
  • Despois indicarase o obxectivo da operación.

Solución Caso Práctico 1 empregando PDO

Arquivo: consultar_libros.php

<?php

ini_set('display_errors', '1');
error_reporting(E_ALL);

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

try {

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

    $libros = $stmt->fetchAll();

} catch (PDOException $e) {

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

} catch (Throwable $e) {

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

}

?>

<!DOCTYPE html>

<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Operacións BD</title>
</head>

<body>

<?php

if ($libros) {

    echo "<table border='1'>";

    foreach ($libros as $libro) {

        echo '<tr>';

        printf(
            '<td>%s</td>',
            htmlspecialchars(
                $libro['titulo'],
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            )
        );

        echo '</tr>';
    }

    echo '</table>';

} else {

    echo 'Non hai datos que amosar.';

}

?>

</body>
</html>
  • Neste caso empregamos `query()` porque a sentenza SQL non recibe ningún dato externo.
  • `fetchAll()` recupera todas as filas do resultado.
Esta solución é adecuada cando sabemos que o número de filas que imos recuperar é pequeno.
  • Se o resultado puidese ter moitas filas, sería preferible procesalas unha a unha:
while ($libro = $stmt->fetch()) {

echo htmlspecialchars(
    $libro['titulo'],
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

}
  • IMPORTANTE: Non empregaremos `rowCount()` para comprobar cantas filas devolveu unha sentenza `SELECT`.
Se realmente necesitamos coñecer o número de rexistros, empregaremos `COUNT(*)`.


Caso Práctico 2

  • Imos recuperar todos os autores ordenados polos seus apelidos e nome.
  • Cada autor aparecerá como un enlace co seguinte formato:
 <a href="detalle_autor.php?id=5">
Apelidos, Nome </a>
  • O identificador será enviado mediante a URL.
Que un identificador apareza na URL non supón por si mesmo un problema de seguridade.
O importante é validar sempre o valor recibido na páxina de destino e comprobar que o usuario ten permiso para acceder ao recurso correspondente.

Arquivo: bd.php

define(
'CONSULTAR_AUTORES',
'SELECT id, apelidos, nome
FROM autores
ORDER BY apelidos, nome'
);

Solución Caso Práctico 2 empregando PDO

Arquivo: consultar_autores.php

<?php

ini_set('display_errors', '1');
error_reporting(E_ALL);

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

try {

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

    $autores = $stmt->fetchAll();

} catch (PDOException $e) {

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

} catch (Throwable $e) {

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

}

?>

<!DOCTYPE html>

<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Autores</title>
</head>

<body>

<?php

if ($autores) {

    echo '<ul>';

    foreach ($autores as $autor) {

        printf(
            "<li><a href='detalle_autor.php?id=%d'>%s, %s</a></li>",
            (int) $autor['id'],
            htmlspecialchars(
                $autor['apelidos'],
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            ),
            htmlspecialchars(
                $autor['nome'],
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            )
        );
    }

    echo '</ul>';

} else {

    echo 'Non hai datos que amosar.';

}

?>

</body>
</html>
  • Para amosar texto dentro de HTML empregamos `htmlspecialchars()`.
  • Non debemos empregar `urlencode()` sobre o nome e os apelidos, xa que estes valores non forman parte da URL senón do texto visible da páxina.


Consulta enviando parámetros de tipo numérico

  • Cando unha consulta necesita recibir datos externos empregaremos unha sentenza preparada mediante:
prepare()
execute()
  • Por exemplo:
SELECT col1, col2
FROM TABOA_1
WHERE col1 = :valor
  • Definimos a consulta:
define(
'CONSULTA_CON_PARAM_NUMERICO',
'SELECT col1, col2
FROM TABOA_1
WHERE col1 = :valor'
);
  • Podemos executala desta forma:
$stmt = $conex->prepare(
CONSULTA_CON_PARAM_NUMERICO
);

$stmt->execute([
'valor' => 5
]);
  • O nome do parámetro debe coincidir co indicado na sentenza SQL.
Se a consulta emprega:
:valor
debemos empregar:
$stmt->execute([
'valor' => 5
]);

Indicar explicitamente o tipo de dato

  • Cando enviamos os valores directamente mediante o array de `execute()`, PDO trátaos por defecto como `PDO::PARAM_STR`.
Na maioría das consultas MySQL realizará as conversións necesarias segundo o contexto.
  • Se queremos indicar explicitamente o tipo empregaremos `bindValue()`:
$stmt = $conex->prepare(
CONSULTA_CON_PARAM_NUMERICO
);

$stmt->bindValue(
':valor',
5,
PDO::PARAM_INT
);

$stmt->execute();
  • Algúns dos tipos dispoñibles son:
PDO::PARAM_INT
PDO::PARAM_STR
PDO::PARAM_BOOL
PDO::PARAM_NULL
  • É especialmente recomendable indicar explicitamente o tipo cando este teña importancia para a consulta, por exemplo con `LIMIT` ou `OFFSET`.


Caso práctico: Enviando parámetros de tipo numérico

  • Imos facer que o usuario poida seleccionar un libro nunha lista `<select>` e que se amosen os exemplares correspondentes.
Terás que programar que o libro se manteña seleccionado cando cambiemos de libro na lista. Para iso recorda que tes o libro selecccionado e cando creas a lista en html terás que comprobar si e o libro seleccionado, dentro do bucle que xera as etiquetas <option>, coincide coa etiqueta option seleccionada e se é o caso, poñela 'selected'.
Php BD 9.jpg



  • Primeiro definimos as consultas.

Arquivo: bd.php

define(
'CONSULTAR_LIBROS',
'SELECT id, titulo
FROM libros
ORDER BY titulo'
);

define(
'CONSULTAR_EXEMPLARES_POR_LIBRO',
'SELECT libro_id,
id_exemplar,
imaxe,
prezo,
estado_id
FROM exemplares
WHERE libro_id = :libro_id
ORDER BY estado_id'
);

define(
'CARTAFOL_IMAXES_WEB',
'http://localhost/IMAXES/'
);
Solución empregando PDO
Fixarse que


Arquivo: consultar_exemplares_por_libro.php

<?php

ini_set('display_errors', '1');
error_reporting(E_ALL);

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

$libros = [];
$exemplares = [];

$libroSeleccionado = filter_input(
    INPUT_POST,
    'lstLibros',
    FILTER_VALIDATE_INT
);

try {

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

    /*
     * Obtemos os libros que se van amosar
     * no elemento select.
     */
    $stmt = $conex->query(CONSULTAR_LIBROS);

    $libros = $stmt->fetchAll();


    /*
     * Se o usuario seleccionou un libro válido,
     * consultamos os seus exemplares.
     */
    if (
        $libroSeleccionado !== false &&
        $libroSeleccionado !== null &&
        $libroSeleccionado !== -1
    ) {

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

        $stmt->bindValue(
            ':libro_id',
            $libroSeleccionado,
            PDO::PARAM_INT
        );

        $stmt->execute();

        $exemplares = $stmt->fetchAll();
    }

} catch (PDOException $e) {

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

} catch (Throwable $e) {

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

}

?>

<!DOCTYPE html>

<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Exemplares por libro</title>
</head>

<body>

<form method="post">

<select
    name="lstLibros"
    onchange="this.form.submit()"
>

    <option value="-1">
        -- Escolle un libro --
    </option>

    <?php foreach ($libros as $libro): ?>

        <option
            value="<?= (int) $libro['id'] ?>"
            <?php
            if (
                $libroSeleccionado !== false &&
                $libroSeleccionado !== null &&
                (int) $libro['id'] === $libroSeleccionado
            ) {
                echo 'selected';
            }
            ?>
        >
            <?= htmlspecialchars(
                $libro['titulo'],
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            ) ?>
        </option>

    <?php endforeach; ?>

</select>

</form>

<?php if (
    $libroSeleccionado !== false &&
    $libroSeleccionado !== null &&
    $libroSeleccionado !== -1
): ?>

<?php if ($exemplares): ?>

    <table border="1">

        <tr>
            <th>Num. exemplar</th>
            <th>Imaxe</th>
            <th>Prezo</th>
            <th>Estado</th>
        </tr>

        <?php foreach ($exemplares as $exemplar): ?>

            <tr>

                <td>
                    <?= (int) $exemplar['id_exemplar'] ?>
                </td>

                <td>
                    <img
                        src="<?= htmlspecialchars(
                            CARTAFOL_IMAXES_WEB .
                            ($exemplar['imaxe'] ?? ''),
                            ENT_QUOTES | ENT_SUBSTITUTE,
                            'UTF-8'
                        ) ?>"
                        alt="<?= htmlspecialchars(
                            $exemplar['imaxe'] ?? '',
                            ENT_QUOTES | ENT_SUBSTITUTE,
                            'UTF-8'
                        ) ?>"
                        width="42"
                        height="42"
                    >
                </td>

                <td>
                    <?= number_format(
                        (float) $exemplar['prezo'],
                        2,
                        ',',
                        '.'
                    ) ?>
                </td>

                <td>
                    <?= (int) $exemplar['estado_id'] ?>
                </td>

            </tr>

        <?php endforeach; ?>

    </table>

<?php else: ?>

    <p>Non hai exemplares que amosar.</p>

<?php endif; ?>

<?php endif; ?>

</body>
</html>
  • Neste exemplo empregamos `filter_input()` con `FILTER_VALIDATE_INT` para comprobar que o dato recibido é un número enteiro válido.
  • Despois utilizamos unha sentenza preparada e:
PDO::PARAM_INT
para indicar explicitamente que o identificador é numérico.
  • Non necesitamos empregar `mysqli_real_escape_string()` nin concatenar o dato dentro da consulta.

Amosar a descrición do estado

  • Podemos mellorar a consulta realizando un `JOIN` coa táboa `estados` para amosar o estado do exemplar.
Php BD 12.jpg


Arquivo: bd.php

define(
'CONSULTAR_EXEMPLARES_POR_LIBRO',
'SELECT exemplares.libro_id,
exemplares.id_exemplar,
exemplares.imaxe,
exemplares.prezo,
estados.descripcion AS desc_estado
FROM exemplares
INNER JOIN estados
ON estados.id = exemplares.estado_id
WHERE exemplares.libro_id = :libro_id
ORDER BY desc_estado'
);
  • Para amosar o estado:
echo htmlspecialchars(
$exemplar['desc_estado'],
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);


Exercicio proposto

  • Crea unha páxina chamada `consultar_exemplares_por_estado.php`.
  • Debe:
  • Amosar os estados da táboa `ESTADOS` mediante botóns de opción (`radio`).
  • Ao seleccionar un estado, realizarase automaticamente o envío do formulario.
  • O estado seleccionado debe manterse marcado.
  • Amosaranse os exemplares correspondentes ao estado seleccionado.
  • A consulta deberá realizarse mediante PDO e unha sentenza preparada.
  • O identificador do estado deberá validarse como número enteiro.
  • Empregarase `PDO::PARAM_INT` para enviar o parámetro.


Php BD 13.jpg
Nota: Podedes darlle un formato máis 'bonito' con CSS.


  • Consultas propostas:
define(
'CONSULTAR_ESTADOS',
'SELECT id, descripcion
FROM estados
ORDER BY descripcion'
);

define(
'CONSULTAR_EXEMPLARES_POR_ESTADO',
'SELECT libros.titulo,
exemplares.id_exemplar,
exemplares.prezo
FROM exemplares
INNER JOIN libros
ON libros.id = exemplares.libro_id
WHERE exemplares.estado_id = :estado_id
ORDER BY libros.titulo'
);


Caso práctico: Enviando parámetros de tipo cadea

  • Imos crear unha páxina que permita buscar libros polo nome dun autor.
O que se pretende programar é algo parecido a isto:
Php BD 11.jpg


O usuario introducirá nunha caixa de texto o nome, os apelidos ou unha parte deles.
Por exemplo, se introduce:
gar
poderían atoparse autores como `García`, `Vargas`, etc., dependendo dos datos existentes na base de datos.
  • Para realizar a busca empregaremos:
  • Un formulario HTML con método `POST`.
  • Unha consulta SQL co operador `LIKE`.
  • Unha sentenza preparada PDO para enviar o texto introducido polo usuario.
  • O parámetro `PDO::PARAM_STR`, xa que o dato enviado é unha cadea.
  • `fetchAll()` para recuperar os resultados.
  • `htmlspecialchars()` cando amosemos datos dentro do HTML.
  • A consulta devolverá:
  • O nome completo do autor.
  • O identificador do libro.
  • O título do libro.
  • Como un mesmo autor pode ter varios libros, evitaremos repetir o seu nome en todas as filas da táboa.
  • Ademais, o título de cada libro será un enlace que permitirá abrir unha nova xanela coa páxina:
detalle_libro.php
á que enviaremos o identificador do libro mediante `GET`.
Para abrir esa páxina nunha nova xanela empregaremos JavaScript e a función `window.open()`.
Polo tanto, o funcionamento xeral será:
Usuario introduce o nome do autor
↓
Validamos o texto
↓
prepare() + bindValue()
↓
Consulta con LIKE
↓
Recuperamos os libros
↓
Amosamos autor e título
↓
Preme sobre un título
↓
window.open()
↓
detalle_libro.php?libro=ID
Arquivo bd.php
  • Engadimos a consulta que permite buscar libros polo nome ou apelidos dun autor:
define(
'CONSULTAR_LIBROS_POR_AUTOR',
"SELECT
CONCAT(
autores.apelidos,
', ',
autores.nome
) AS nome_autor,
libros.id AS id_libro,
libros.titulo
FROM libros
INNER JOIN libro_autor
ON libro_autor.libro_id = libros.id
INNER JOIN autores
ON libro_autor.autor_id = autores.id
WHERE CONCAT(
autores.nome,
' ',
autores.apelidos
) LIKE :autor
ORDER BY nome_autor, libros.titulo"
);
  • A consulta une as táboas:
libros
libro_autor
autores
para obter os libros correspondentes a cada autor.
  • Mediante:
CONCAT(
autores.nome,
' ',
autores.apelidos
)
construímos unha cadea formada polo nome e os apelidos do autor.
  • Sobre esa cadea aplicamos:
LIKE :autor
sendo `:autor` o parámetro que enviaremos posteriormente desde PDO.
  • O parámetro debe representar o valor completo.
Polo tanto, non escribiremos:
LIKE '%:autor%'
senón que os caracteres `%` serán engadidos ao valor desde PHP:
'%' . $textoBuscar . '%'
Solución completa

Arquivo: buscar_libros_por_autor.php

<?php

/*
 * Estas directivas utilízanse durante o desenvolvemento.
 * Nun servidor en produción non se deberían amosar
 * os erros PHP ao usuario.
 */
ini_set('display_errors', '1');
error_reporting(E_ALL);

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


/*
 * Variables que utilizaremos posteriormente no HTML.
 */
$erro = '';
$libros = [];


/*
 * Recuperamos o valor enviado polo formulario.
 *
 * O operador ?? permite indicar unha cadea baleira
 * se o formulario aínda non foi enviado.
 *
 * trim() elimina espazos en branco ao principio
 * e ao final da cadea.
 */
$textoBuscar = trim(
    $_POST['txtAutor'] ?? ''
);


try {

    /*
     * Creamos o DSN.
     */
    $dsn = sprintf(
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
        SERVIDOR,
        BD
    );


    /*
     * Abrimos a conexión.
     *
     * Os datos SERVIDOR, USUARIO, PASSWORD e BD
     * están definidos no arquivo bd.php.
     */
    $conex = new PDO(
        $dsn,
        USUARIO,
        PASSWORD,
        [
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]
    );


    /*
     * Comprobamos se o formulario foi enviado.
     */
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {

        /*
         * Comprobamos que se introduciu algún texto.
         */
        if ($textoBuscar === '') {

            $erro = 'Debes introducir un texto para realizar a busca.';

            /*
         * Como regra da aplicación esiximos
         * polo menos 3 caracteres.
         */
        } elseif (mb_strlen($textoBuscar) < 3) {

            $erro =
                'A cadea a buscar debe ter polo menos 3 caracteres.';
        } else {

            /*
             * Preparamos a consulta.
             */
            $stmt = $conex->prepare(
                CONSULTAR_LIBROS_POR_AUTOR
            );


            /*
             * Engadimos os comodíns % de LIKE ao valor
             * que enviamos ao parámetro.
             *
             * Por exemplo:
             *
             * gar
             *
             * convértese en:
             *
             * %gar%
             */
            $stmt->bindValue(
                ':autor',
                '%' . $textoBuscar . '%',
                PDO::PARAM_STR
            );


            /*
             * Executamos a consulta.
             */
            $stmt->execute();


            /*
             * Recuperamos todos os resultados.
             */
            $libros = $stmt->fetchAll();


            /*
             * Se non hai filas gardamos unha mensaxe
             * para amosala posteriormente.
             */
            if (!$libros) {

                $erro =
                    'Non se atoparon libros para ese autor.';
            }
        }
    }
} catch (PDOException $e) {

    /*
     * Rexistramos a información técnica no log.
     * Non debemos amosala ao usuario.
     */
    error_log(
        $e->getMessage()
    );

    die('Erro relacionado coa base de datos.');
} catch (Throwable $e) {

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

?>

<!DOCTYPE html>

<html lang="gl">

    <head>

        <meta charset="UTF-8">
        <title>Buscar libros por autor</title>
        <script>
            /*
            * Recibe a URL que queremos abrir
            * e solicita ao navegador unha nova xanela.
            */
            function abrirVentana(url) {

                window.open(
                    url,
                    '_blank',
                    'popup,width=600,height=400,noopener'
                );
            }
        </script>
    </head>

<body>

    <h1>Buscar libros por autor</h1>
    <form method="post">

        <label for="txtAutor">
            Introduce o nome ou apelidos do autor:
        </label>
        <input
            type="text"
            id="txtAutor"
            name="txtAutor"
            size="50"
            maxlength="100"
            required
            value="<?= htmlspecialchars(
                        $textoBuscar,
                        ENT_QUOTES | ENT_SUBSTITUTE,
                        'UTF-8'
                    ) ?>">
        <button
            type="submit"
            value="BUSCAR">Buscar</button>
        <input
            type="reset"
            value="LIMPAR">

    </form>

    <?php if ($erro !== ''): ?>
        <p>

            <?= htmlspecialchars(
                $erro,
                ENT_QUOTES | ENT_SUBSTITUTE,
                'UTF-8'
            ) ?>
        </p>
    <?php endif; ?>

    <?php if ($libros): ?>
        <table border="1">
            <tr>
                <th>Autor</th>
                <th>Libro</th>
            </tr>
            <?php

         /*
         * Esta variable permite saber cal foi
         * o último autor que amosamos.
         */
            $ultimoAutor = '';
            foreach ($libros as $libro):
             /*
             * Se o autor desta fila é o mesmo
             * que o da fila anterior,
             * non volvemos amosar o seu nome.
             */
                if (
                    $ultimoAutor ===
                    $libro['nome_autor']
                ) {

                    $autorAmosar = '';
                } else {

                    $autorAmosar =
                        $libro['nome_autor'];
                }

            ?>
                <tr>
                    <td>
                        <?= htmlspecialchars(
                            $autorAmosar,
                            ENT_QUOTES | ENT_SUBSTITUTE,
                            'UTF-8'
                        ) ?>
                    </td>
                    <td>
                        <a
                            href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>"
                            onclick="abrirVentana(this.href); return false;">

                            <?= htmlspecialchars(
                                $libro['titulo'],
                                ENT_QUOTES | ENT_SUBSTITUTE,
                                'UTF-8'
                            ) ?>
                        </a>
                    </td>
                </tr>
            <?php

             /*
             * Gardamos o autor actual para comparalo
             * co autor da seguinte fila.
             */
                $ultimoAutor =
                    $libro['nome_autor'];

            endforeach;
            ?>
        </table>
    <?php endif; ?>
</body>
</html>
Explicación da solución
  • Recuperamos o texto enviado polo formulario mediante:
$textoBuscar = trim(
$_POST['txtAutor'] ?? ''
);
  • Non empregamos `htmlspecialchars()` neste momento.
Neste punto estamos validando e procesando un dato de entrada.
`htmlspecialchars()` empregarase posteriormente cando o dato se introduza dentro do HTML.



Validación do dato

  • Comprobamos primeiro que a cadea non estea baleira:
if ($textoBuscar === '') {
  • Como exemplo dunha regra de validación da aplicación, esiximos tamén unha lonxitude mínima:
elseif (mb_strlen($textoBuscar) < 3) {
A esixencia de tres caracteres é unha regra da nosa aplicación.
Non é unha medida para evitar inxección SQL.



Preparación da consulta

  • A consulta recibe datos procedentes do usuario, polo que debemos empregar unha sentenza preparada:
$stmt = $conex->prepare(
CONSULTAR_LIBROS_POR_AUTOR
);



Asociación do parámetro

  • Asociamos o valor ao parámetro `:autor`:
$stmt->bindValue(
':autor',
'%' . $textoBuscar . '%',
PDO::PARAM_STR
);
  • Indicamos:
PDO::PARAM_STR
porque estamos enviando unha cadea.



Funcionamento de LIKE

  • Os caracteres `%` actúan como comodíns.
Se o usuario introduce:
gar
PHP enviará:
%gar%
  • Desta forma poderán coincidir cadeas nas que `gar` apareza ao principio, no medio ou ao final.



Execución e recuperación dos resultados

  • Executamos:
$stmt->execute();
  • E recuperamos os resultados:
$libros = $stmt->fetchAll();
  • Como na conexión configuramos:
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
cada fila recuperada será un array asociativo.



Evitar repetir o nome do autor

  • Pode suceder que un mesmo autor teña varios libros.
Por exemplo:
García, Ana | Libro A
García, Ana | Libro B
García, Ana | Libro C
Pérez, Luís | Libro D
  • Queremos amosalo desta forma:
García, Ana | Libro A
            | Libro B
            | Libro C
Pérez, Luís | Libro D
  • Para iso gardamos o último autor procesado:
$ultimoAutor = '';
  • En cada iteración comparamos:
if ($ultimoAutor ===$libro['nome_autor']) {

   $autorAmosar = '';

} else {
    $autorAmosar = $libro['nome_autor'];
}
  • E ao rematar gardamos o autor actual:
$ultimoAutor =
$libro['nome_autor'];



Abrir o detalle dun libro

  • Cada título será un enlace á páxina:
detalle_libro.php
  • O identificador enviarase mediante `GET`:
detalle_libro.php?libro=5


  • O enlace xerado será:
<a href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>" onclick="abrirVentana(this.href); return false;">
  • Mediante:
this.href
obtemos a URL real almacenada no atributo `href`.
  • Chamamos despois á función:
abrirVentana(this.href);
  • Finalmente:
return false;
evita que o navegador abra tamén o enlace na xanela actual.



Función JavaScript

  • A función empregada é:
function abrirVentana(url) {

 window.open(
    url,
    '_blank',
    'popup,width=600,height=400,noopener'
 );

}
  • `window.open()` solicita ao navegador a apertura dunha nova xanela.
  • Indicamos aproximadamente:
width=600
height=400
  • A opción:
noopener
evita que a nova xanela poida acceder á xanela orixinal mediante `window.opener`.

NOTA: O navegador ten a decisión final sobre se a páxina se abre nunha nova xanela ou nunha pestana e pode ignorar algunhas características solicitadas.



Vantaxe de empregar un href real

  • Non empregamos:
 <a href="#">
  • Empregamos:
 <a href="detalle_libro.php?libro=5">
Desta forma, se JavaScript non funciona ou está desactivado, o enlace segue sendo válido e o usuario poderá acceder igualmente ao detalle do libro.



Páxina detalle_libro.php

  • A páxina de detalle recibirá o identificador do libro mediante:
$_GET['libro']
  • Como é un dato externo, debemos validalo:
$libroId = filter_input(
INPUT_GET,
'libro',
FILTER_VALIDATE_INT
);

if (
  $libroId === false ||
  $libroId === null ||
  $libroId < 1
) {

  die('Identificador de libro non válido.');

}


  • A consulta posterior tamén deberá ser preparada:
$stmt = $conex->prepare(
  'SELECT id, titulo
   FROM libros
   WHERE id = :libro_id'
);

$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
$stmt->execute();

$libro = $stmt->fetch();
  • Se queremos recuperar tamén os exemplares dese libro faremos outra consulta preparada:
$stmt = $conex->prepare(
 'SELECT id_exemplar,imaxe,prezo,estado_id
  FROM exemplares
  WHERE libro_id = :libro_id
  ORDER BY id_exemplar'
);

$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
$stmt->execute();

$exemplares = $stmt->fetchAll();
Resumo do proceso
$_POST['txtAutor']
↓
trim()
↓
validación
↓
prepare()
↓
bindValue(PDO::PARAM_STR)
↓
execute()
↓
fetchAll()
↓
htmlspecialchars()
↓
enlace ao libro
↓
window.open()
↓
detalle_libro.php?libro=ID
↓
FILTER_VALIDATE_INT
↓
prepare() + PDO::PARAM_INT

</syntaxhighlight>


  • Debemos diferenciar claramente:
! Operación

| ! Finalidade                                           |
| ------------------------------------------------------ |
| `trim()` e comprobación da lonxitude                   |
| Validar os datos segundo as regras da aplicación.      |
| -                                                      |
| `prepare()` + `bindValue()`                            |
| Separar os datos da estrutura da sentenza SQL.         |
| -                                                      |
| `PDO::PARAM_STR`                                       |
| Indicar que o parámetro enviado é unha cadea.          |
| -                                                      |
| `htmlspecialchars()`                                   |
| Escapar datos cando se introducen dentro de HTML.      |
| -                                                      |
| `window.open()`                                        |
| Abrir o detalle do libro nunha nova xanela ou pestana. |
| -                                                      |
| `FILTER_VALIDATE_INT`                                  |
| Validar o identificador recibido por `GET`.            |
| }                                                      |



Consulta chamando a procedementos almacenados

  • Un procedemento almacenado permite gardar no propio xestor de base de datos un conxunto de sentenzas SQL que poden ser executadas posteriormente.
  • Desde PDO chamarémolo mediante a sentenza:
CALL NOME_PROCEDEMENTO()
  • Por exemplo, en MySQL:
DELIMITER $$

CREATE PROCEDURE CONSULTAR_LIBROS()
BEGIN

SELECT id, titulo
FROM libros
ORDER BY titulo;

END$$

DELIMITER ;

Empregando PDO

$stmt = $conex->prepare(
'CALL CONSULTAR_LIBROS()'
);

$stmt->execute();

$libros = $stmt->fetchAll();

$stmt->closeCursor();
  • `closeCursor()` libera os resultados pendentes asociados á chamada.
É especialmente recomendable ao traballar con procedementos almacenados en MySQL antes de executar outra consulta na mesma conexión.

Caso Práctico: Consulta chamando a procedementos almacenados

  • Definimos o nome do procedemento:
define(
'PROCEDEMENTO_CONSULTAR_LIBROS',
'CONSULTAR_LIBROS'
);
  • Realizamos a chamada:
$stmt = $conex->prepare(
'CALL ' .
PROCEDEMENTO_CONSULTAR_LIBROS .
'()'
);

$stmt->execute();

$libros = $stmt->fetchAll();

$stmt->closeCursor();
  • Non necesitamos comprobar o resultado de `execute()` para saber se o `SELECT` devolveu filas.
`execute()` indica se a sentenza puido executarse.
Para saber se temos resultados comprobamos os datos recuperados:
if ($libros) {


// Hai resultados

} else {

// Non hai resultados

}


Consulta chamando a procedementos almacenados con parámetros de entrada

  • Os parámetros dun procedemento almacenado poden ser:
  • `IN`: parámetros de entrada.
  • `OUT`: parámetros de saída.
  • `INOUT`: funcionan como entrada e saída.
  • Por exemplo:
DELIMITER $$

CREATE PROCEDURE PROCEDEMENTO_CON_PARAMETROS(
IN p_num INT,
IN p_text VARCHAR(100)
)
BEGIN

SELECT col1, col2
FROM TABOA_1
WHERE col1 = p_num
  AND col2 = p_text;

END$$

DELIMITER ;

Empregando PDO

  • Preparamos a chamada igual que calquera outra sentenza parametrizada:
$stmt = $conex->prepare(
'CALL PROCEDEMENTO_CON_PARAMETROS(
:param_num,
:param_text
)'
);

$stmt->bindValue(
':param_num',
$valorNum,
PDO::PARAM_INT
);

$stmt->bindValue(
':param_text',
$valorTexto,
PDO::PARAM_STR
);

$stmt->execute();

$filas = $stmt->fetchAll();

$stmt->closeCursor();
  • Desta forma non necesitamos empregar `sprintf()` nin concatenar os valores dentro da chamada SQL.

Caso Práctico: buscar libros por autor

  • Lembrar que o usuario que se conecta á base de datos dende php ten que ter permiso execute sobre a base de datos.
 grant execute on php.* to 'user_php'@'localhost';
 flush privileges;


  • Procedemento almacenado (isto tedes que executalo en Mysql empregando a ferramenta Mysql WorkBench ou PhpMyAdmin):
DELIMITER $$

CREATE PROCEDURE CONSULTAR_LIBROS_POR_AUTOR(
IN p_autor_buscar VARCHAR(100)
)
BEGIN

SELECT CONCAT(
           autores.apelidos,
           ', ',
           autores.nome
       ) AS nome_autor,
       libros.id AS id_libro,
       libros.titulo
FROM libros
INNER JOIN libro_autor
    ON libro_autor.libro_id = libros.id
INNER JOIN autores
    ON libro_autor.autor_id = autores.id
WHERE CONCAT(
          autores.nome,
          ' ',
          autores.apelidos
      ) LIKE CONCAT(
          '%',
          p_autor_buscar,
          '%'
      )
ORDER BY nome_autor, libros.titulo;

END$$

DELIMITER ;
  • Chamada desde PDO cambiando no exercicio anterior a chamada ao SELECT pola chamada a este procedemento almacenado:
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {

        /*
         * Comprobamos que se introduciu algún texto.
         */
        if ($textoBuscar === '') {

            $erro = 'Debes introducir un texto para realizar a busca.';

            /*
         * Como regra da aplicación esiximos
         * polo menos 3 caracteres.
         */
        } elseif (mb_strlen($textoBuscar) < 3) {

            $erro =
                'A cadea a buscar debe ter polo menos 3 caracteres.';
        } else {

            /*
             * Preparamos a consulta.
             */
            $stmt = $conex->prepare(
                'call CONSULTAR_LIBROS_POR_AUTOR(:autor)'
            );


            /*
             * Engadimos os comodíns % de LIKE ao valor
             * que enviamos ao parámetro.
             *
             * Por exemplo:
             *
             * gar
             *
             * convértese en:
             *
             * %gar%
             */
            $stmt->bindValue(
                ':autor',
                '%' . $textoBuscar . '%',
                PDO::PARAM_STR
            );


            /*
             * Executamos a consulta.
             */
            $stmt->execute();


            /*
             * Recuperamos todos os resultados.
             */
            $libros = $stmt->fetchAll();

            $stmt->closeCursor();


            /*
             * Se non hai filas gardamos unha mensaxe
             * para amosala posteriormente.
             */
            if (!$libros) {

                $erro =
                    'Non se atoparon libros para ese autor.';
            }
        }
    }


Consulta chamando a procedementos almacenados con parámetros de saída

  • Un parámetro `OUT` permite que un procedemento devolva un valor.
  • Por exemplo:
DELIMITER $$

CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
OUT p_num_libros INT
)
BEGIN

SELECT COUNT(*)
INTO p_num_libros
FROM libros;

END$$

DELIMITER ;
  • Con PDO e MySQL podemos empregar unha variable de usuario de MySQL:
$stmt = $conex->prepare(
'CALL CONSULTAR_NUM_LIBROS(
@numero_libros
)'
);

$stmt->execute();

/*

* É importante liberar os resultados
* da chamada antes de executar outro SELECT.
  */
  $stmt->closeCursor();

$stmt = $conex->query(
'SELECT @numero_libros'
);

$numLibros = (int) $stmt->fetchColumn();
  • Podemos amosar o resultado:
printf(
'<h1>Número de libros á venda: %d</h1>',
$numLibros
);


Consulta chamando a funcións

  • MySQL tamén permite definir funcións almacenadas.
  • Unha función, a diferenza dun procedemento, devolve directamente un valor e pode empregarse dentro dunha expresión SQL.
  • Por exemplo:
DELIMITER $$

CREATE FUNCTION CONSULTAR_NUM_LIBROS()
RETURNS INT
READS SQL DATA
BEGIN

DECLARE num_libros INT;

SELECT COUNT(*)
INTO num_libros
FROM libros;

RETURN num_libros;

END$$

DELIMITER ;

Empregando PDO

  • Podemos chamar á función mediante un `SELECT`:
$stmt = $conex->query(
'SELECT CONSULTAR_NUM_LIBROS()'
);

$numLibros = (int) $stmt->fetchColumn();
  • `fetchColumn()` resulta especialmente útil cando unha consulta devolve un único valor.

Funcións con parámetros

  • Supoñamos unha función:
SELECT Suma(5, 7)
  • Desde PDO:
$stmt = $conex->prepare(
'SELECT Suma(:a, :b)'
);

$stmt->bindValue(
':a',
5,
PDO::PARAM_INT
);

$stmt->bindValue(
':b',
7,
PDO::PARAM_INT
);

$stmt->execute();

$resultado = $stmt->fetchColumn();


Paxinación de resultados

  • En moitas ocasións unha consulta pode devolver unha cantidade elevada de rexistros.
Para evitar amosalos todos simultaneamente podemos dividir o resultado en páxinas.
  • En MySQL podemos empregar `LIMIT` e `OFFSET`.
  • Por exemplo:
SELECT id, titulo
FROM libros
ORDER BY titulo
LIMIT 2 OFFSET 0;
Devolverá os dous primeiros rexistros.
SELECT id, titulo
FROM libros
ORDER BY titulo
LIMIT 2 OFFSET 2;
Devolverá os dous seguintes.

Pasos necesarios para realizar a paxinación

  • Necesitamos:
  1. Coñecer o número total de rexistros.
  2. Establecer cantos rexistros se amosarán por páxina.
  3. Coñecer a páxina actual.
  4. Calcular o número total de páxinas.
  5. Calcular o desprazamento (`OFFSET`).
  6. Realizar a consulta correspondente.



Número total de rexistros

$stmt = $conex->query(
'SELECT COUNT(*)
FROM libros'
);

$numLibros = (int) $stmt->fetchColumn();



Número de rexistros por páxina

define(
'CONST_NUM_LIBROS_PAXINA',
10
);



Obter e validar a páxina actual

  • O número de páxina chegará normalmente mediante `GET`.
$paxinaActual = filter_input(
INPUT_GET,
'paxina',
FILTER_VALIDATE_INT
);

if (
$paxinaActual === false ||
$paxinaActual === null ||
$paxinaActual < 1
) {

$paxinaActual = 1;

}
  • `htmlspecialchars()` non serve para validar que un dato sexa numérico.
Neste caso empregamos `FILTER_VALIDATE_INT`.



Número total de páxinas

$totalPaxinas = (int) ceil(
$numLibros /
CONST_NUM_LIBROS_PAXINA
);
  • Podemos evitar que o usuario solicite unha páxina superior á existente:
if (
$totalPaxinas > 0 &&
$paxinaActual > $totalPaxinas
) {

$paxinaActual = $totalPaxinas;

}



Calcular o OFFSET

$inicio = (
$paxinaActual - 1
) * CONST_NUM_LIBROS_PAXINA;



Consulta paxinada

  • Como `LIMIT` e `OFFSET` necesitan valores numéricos, indicaremos explicitamente o tipo mediante `PDO::PARAM_INT`.
$stmt = $conex->prepare(
'SELECT id, titulo
FROM libros
ORDER BY titulo
LIMIT :limite
OFFSET :inicio'
);

$stmt->bindValue(
':limite',
CONST_NUM_LIBROS_PAXINA,
PDO::PARAM_INT
);

$stmt->bindValue(
':inicio',
$inicio,
PDO::PARAM_INT
);

$stmt->execute();

$libros = $stmt->fetchAll();



Amosar os resultados

if ($libros) {

echo "<table border='1'>";

foreach ($libros as $libro) {

    echo '<tr>';

    printf(
        '<td>%s</td>',
        htmlspecialchars(
            $libro['titulo'],
            ENT_QUOTES | ENT_SUBSTITUTE,
            'UTF-8'
        )
    );

    echo '</tr>';
}

echo '</table>';

} else {

echo 'Non hai datos que amosar.';

}



Amosar os enlaces ás páxinas

echo '<div>';

for (
$cont = 1;
$cont <= $totalPaxinas;
$cont++
) {

if ($cont === $paxinaActual) {

    printf(
        '<strong>%d</strong> ',
        $cont
    );

} else {

    printf(
        "<a href='?paxina=%d'>%d</a> ",
        $cont,
        $cont
    );
}

}

echo '</div>';


Exemplo completo de paxinación

Aquivo bd.php

define(
    'CONST_NUM_LIBROS_PAXINA',
    10
);
::


Páxina php a cargar

<?php

ini_set('display_errors', '1');
error_reporting(E_ALL);

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


try {

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


    /*
     * Número total de libros.
     */
    $stmt = $conex->query(
        'SELECT COUNT(*)
         FROM libros'
    );

    $numLibros = (int) $stmt->fetchColumn();


    /*
     * Número total de páxinas.
     */
    $totalPaxinas = (int) ceil(
        $numLibros /
        CONST_NUM_LIBROS_PAXINA
    );


    /*
     * Páxina solicitada polo usuario.
     */
    $paxinaActual = filter_input(
        INPUT_GET,
        'paxina',
        FILTER_VALIDATE_INT
    );

    if (
        $paxinaActual === false ||
        $paxinaActual === null ||
        $paxinaActual < 1
    ) {

        $paxinaActual = 1;
    }


    if (
        $totalPaxinas > 0 &&
        $paxinaActual > $totalPaxinas
    ) {

        $paxinaActual = $totalPaxinas;
    }


    /*
     * Primeiro rexistro da páxina.
     */
    $inicio = (
        $paxinaActual - 1
    ) * CONST_NUM_LIBROS_PAXINA;


    /*
     * Consulta dos libros da páxina actual.
     */
    $stmt = $conex->prepare(
        'SELECT id, titulo
         FROM libros
         ORDER BY titulo
         LIMIT :limite
         OFFSET :inicio'
    );

    $stmt->bindValue(
        ':limite',
        CONST_NUM_LIBROS_PAXINA,
        PDO::PARAM_INT
    );

    $stmt->bindValue(
        ':inicio',
        $inicio,
        PDO::PARAM_INT
    );

    $stmt->execute();

    $libros = $stmt->fetchAll();

} catch (PDOException $e) {

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

} catch (Throwable $e) {

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

}

?>

<!DOCTYPE html>

<html lang="gl">

<head>
    <meta charset="UTF-8">
    <title>Paxinación de libros</title>
</head>

<body>

<?php if ($libros): ?>

<table border="1">

    <?php foreach ($libros as $libro): ?>

        <tr>
            <td>
                <?= htmlspecialchars(
                    $libro['titulo'],
                    ENT_QUOTES | ENT_SUBSTITUTE,
                    'UTF-8'
                ) ?>
            </td>
        </tr>

    <?php endforeach; ?>

</table>

<?php else: ?>

<p>Non hai libros que amosar.</p>

<?php endif; ?>

<?php if ($totalPaxinas > 1): ?>

<div>

    <?php
    for (
        $cont = 1;
        $cont <= $totalPaxinas;
        $cont++
    ):
    ?>

        <?php if ($cont === $paxinaActual): ?>

            <strong>
                <?= $cont ?>
            </strong>

        <?php else: ?>

            <a href="?paxina=<?= $cont ?>">
                <?= $cont ?>
            </a>

        <?php endif; ?>

    <?php endfor; ?>

</div>

<?php endif; ?>

</body>
</html>

Sobre a optimización da paxinación

  • En xeral non debemos enviar pola URL o número total de rexistros e confiar posteriormente nese dato:
?paxina=2&numtotal=50
Os datos enviados mediante `GET` poden ser modificados polo usuario.
  • Gardar o número total de rexistros nunha variable de sesión tampouco garante que o dato siga sendo correcto, xa que outros usuarios poderían inserir ou eliminar rexistros.
  • Como primeira solución realizaremos en cada petición:
SELECT COUNT(*)
FROM libros
  • Se nunha aplicación real esta operación chegase a ser un problema de rendemento poderían estudarse técnicas de caché ou outras estratexias de optimización.


Exercicio proposto

  • Crea unha páxina chamada `buscar_libros.php` que permita buscar libros empregando os seguintes criterios:
  • Título: unha caixa de texto que realizará unha busca mediante `LIKE`.
  • Prezo: un campo no que se validará que o valor introducido sexa numérico.
  • Estado: unha lista `<select>` cos estados dispoñibles.
  • Autor: unha lista `<select>` cos autores dispoñibles.
  • Criterio de ordenación: título, prezo ou autor.
  • O usuario poderá combinar os diferentes criterios.
  • Os resultados deberán poder amosarse paxinados.
  • A solución deberá:
  • Empregar PDO.
  • Empregar sentenzas preparadas para todos os datos externos.
  • Validar os valores recibidos mediante `GET` ou `POST`.
  • Empregar `bindValue()` co tipo correspondente cando sexa necesario.
  • Non concatenar directamente datos introducidos polo usuario dentro dunha sentenza SQL.
  • Empregar `htmlspecialchars()` cando se amosen cadeas dentro do HTML.
  • Capturar os erros PDO mediante `PDOException`.
  • Rexistrar os detalles do erro mediante `error_log()`.
  • Amosar ao usuario unha mensaxe xenérica en caso de erro.
  • Capturar outros erros da aplicación mediante `Throwable`.


Resumo das funcións PDO empregadas

! Método / constante

| ! Uso                                                                                             |
| ------------------------------------------------------------------------------------------------- |
| `new PDO(...)`                                                                                    |
| Abrir unha conexión coa base de datos.                                                            |
| -                                                                                                 |
| `query()`                                                                                         |
| Executar unha sentenza SQL sen parámetros externos e obter un resultado.                          |
| -                                                                                                 |
| `prepare()`                                                                                       |
| Preparar unha sentenza SQL parametrizada.                                                         |
| -                                                                                                 |
| `execute()`                                                                                       |
| Executar unha sentenza preparada.                                                                 |
| -                                                                                                 |
| `bindValue()`                                                                                     |
| Asociar un valor e, opcionalmente, o seu tipo a un parámetro.                                     |
| -                                                                                                 |
| `fetch()`                                                                                         |
| Recuperar unha fila do resultado.                                                                 |
| -                                                                                                 |
| `fetchAll()`                                                                                      |
| Recuperar todas as filas do resultado.                                                            |
| -                                                                                                 |
| `fetchColumn()`                                                                                   |
| Recuperar o valor dunha columna, especialmente útil para `COUNT(*)` ou funcións.                  |
| -                                                                                                 |
| `closeCursor()`                                                                                   |
| Liberar os resultados pendentes dunha sentenza, especialmente útil con procedementos almacenados. |
| -                                                                                                 |
| `PDO::PARAM_INT`                                                                                  |
| Indica un parámetro de tipo enteiro.                                                              |
| -                                                                                                 |
| `PDO::PARAM_STR`                                                                                  |
| Indica un parámetro de tipo cadea.                                                                |
| -                                                                                                 |
| `PDO::PARAM_BOOL`                                                                                 |
| Indica un parámetro booleano.                                                                     |
| -                                                                                                 |
| `PDO::PARAM_NULL`                                                                                 |
| Indica un valor nulo.                                                                             |
| -                                                                                                 |
| `PDO::FETCH_ASSOC`                                                                                |
| Recupera as filas como arrays asociativos.                                                        |
| }                                                                                                 |
::

Regras que empregaremos nos exames

  1. As operacións contra a base de datos realizaranse mediante PDO.
  1. Para consultas sen parámetros externos poderá empregarse `query()`.
  1. Se hai datos procedentes do usuario ou outros valores externos empregarase `prepare()` + `execute()`.
  1. Non se concatenarán directamente datos externos nas sentenzas SQL.
  1. Os datos recibidos mediante formularios ou URL deberán validarse.
  1. Cando o tipo sexa relevante empregarase `bindValue()` indicando `PDO::PARAM_INT`, `PDO::PARAM_STR`, etc.
  1. Non se empregará `rowCount()` para coñecer o número de filas devoltas por un `SELECT`.
  1. Se necesitamos contar filas empregaremos `COUNT(*)`.
  1. Para amosar cadeas dentro de HTML empregarase `htmlspecialchars()`.
  1. Os erros PDO capturaranse mediante `PDOException`.
  1. Os detalles técnicos dos erros rexistraranse mediante `error_log()` e non se amosarán ao usuario final.
  1. Os demais erros ou excepcións poderán capturarse mediante `Throwable`.
  1. Os exemplos realizaranse tomando como referencia PHP 8.5.






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